{text}
' + + +def strike_through(string: str) -> str: + """Return a striked-through html span from given ``string``.""" + if match := RE_DESCR_DELETED_PATTERN.match(string): + string = match.group(1) + return f'{string}' + + +def generate_image_html( + title: str, attachment_id: str, max_width: int, cls: str +) -> str: + """Generate an image as HTMl with the given source.""" + description = ( + f'' + ) + return description + + +def camel_case_to_words(camel_case_str: str): + """Split camel or dromedary case and return it as a space separated str.""" + return ( + camel_case_str[0].capitalize() + + " ".join( + re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)", camel_case_str) + )[1:] + ) + + +class JinjaRendererMixin: + """A MixIn for converters which should render jinja frequently.""" + + jinja_envs: dict[str, jinja2.Environment] + + def _get_jinja_env(self, template_folder: str | pathlib.Path): + template_folder = str(template_folder) + if env := self.jinja_envs.get(template_folder): + return env + + env = jinja2.Environment( + loader=jinja2.FileSystemLoader(template_folder) + ) + self.setup_env(env) + + self.jinja_envs[template_folder] = env + return env + + def check_model_element( + self, obj: object + ) -> ( + capellambse.model.GenericElement + | capellambse.model.diagram.AbstractDiagram + | None + ): + """Check if a model element was passed. + + Return None if no element and raise a TypeError if a wrong typed + element was passed. Returns the element if it matches + expectations. + """ + if jinja2.is_undefined(obj) or obj is None: + return None + + if isinstance(obj, capellambse.model.ElementList): + raise TypeError("Cannot make an href to a list of elements") + if not isinstance( + obj, + ( + capellambse.model.GenericElement, + capellambse.model.diagram.AbstractDiagram, + ), + ): + raise TypeError(f"Expected a model object, got {obj!r}") + return obj + + def setup_env(self, env: jinja2.Environment): + """Implement this method to adjust a newly created environment.""" + pass diff --git a/jupyter-notebooks/document_generation.ipynb b/jupyter-notebooks/document_generation.ipynb new file mode 100644 index 0000000..21d0b7e --- /dev/null +++ b/jupyter-notebooks/document_generation.ipynb @@ -0,0 +1,184 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "3f2aa85c-bd79-4e81-a851-c0197acfc3b3", + "metadata": {}, + "outputs": [], + "source": [ + "from capella2polarion.connectors import polarion_worker\n", + "from capella2polarion.converters import document_renderer\n", + "import dotenv\n", + "import os\n", + "import capellambse" + ] + }, + { + "cell_type": "markdown", + "id": "e3f3d9b0-6738-49e0-b1a7-75105f483ebb", + "metadata": {}, + "source": [ + "Create a `.env` file with the following values:\n", + "- POLARION_PROJECT\n", + "- POLARION_HOST\n", + "- POLARION_PAT\n", + "- MODEL_PATH" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eedbd73e-aa06-486f-b0d4-7c0daaa9fb62", + "metadata": {}, + "outputs": [], + "source": [ + "dotenv.load_dotenv()\n", + "model = capellambse.MelodyModel(os.environ.get(\"MODEL_PATH\"))\n", + "worker = polarion_worker.CapellaPolarionWorker(\n", + " polarion_worker.PolarionWorkerParams(\n", + " os.environ.get(\"POLARION_PROJECT\"),\n", + " os.environ.get(\"POLARION_HOST\"),\n", + " os.environ.get(\"POLARION_PAT\"),\n", + " False,\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "248750da-ce16-434c-8eb4-009aaf42fa55", + "metadata": {}, + "outputs": [], + "source": [ + "worker.load_polarion_work_item_map()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5f90a03c-15ad-40c0-8132-9073e3e5aa44", + "metadata": {}, + "outputs": [], + "source": [ + "renderer = document_renderer.DocumentRenderer(worker.polarion_data_repo, model)" + ] + }, + { + "cell_type": "markdown", + "id": "c9e3889d-063d-480f-86e8-412af898c426", + "metadata": {}, + "source": [ + "If the document, we want to render already exists in Polarion, we should request it before we re-render it." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9366113e-6b67-46e7-9d68-44b3f2fde61d", + "metadata": {}, + "outputs": [], + "source": [ + "old_doc=worker.client.project_client.documents.get(\"_default\", \"TEST-ICD1\", fields={\"documents\":\"@all\"})" + ] + }, + { + "cell_type": "markdown", + "id": "542dd1db-2b12-410e-9588-a67b66f7db98", + "metadata": {}, + "source": [ + "In this example we want to create a document to describe an interface. The template expects the UUID of a component exchange. As we know that the document already exists in Polarion, we pass it to the renderer, to let it reuse existing heading workitems. The workitems which should be updated are returned in addition to the newly generated document." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "09742719-766d-40ca-b344-5235dca88933", + "metadata": {}, + "outputs": [], + "source": [ + "new_doc, wis=renderer.render_document(\n", + " \"document_templates\", \n", + " \"test-icd.html.j2\", \n", + " document=old_doc, \n", + " interface=\"3d21ab4b-7bf6-428b-ba4c-a27bca4e86db\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "14f2799a-ba4d-4707-ad3a-36e485cd8065", + "metadata": {}, + "outputs": [], + "source": [ + "worker.client.project_client.work_items.update(wis)\n", + "worker.client.project_client.documents.update(new_doc)" + ] + }, + { + "cell_type": "markdown", + "id": "7f204e55-1d6d-4ce8-87a2-08520be45a05", + "metadata": {}, + "source": [ + "If we want to create a new document, we don't have to pass a document" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "8756d66b-25e3-48db-bee7-3d321f3d5957", + "metadata": {}, + "outputs": [], + "source": [ + "new_doc, wis=renderer.render_document(\n", + " \"document_templates\", \n", + " \"test-icd.html.j2\", \n", + " \"_default\", \n", + " \"TEST-ICD5\", \n", + " interface=\"3d21ab4b-7bf6-428b-ba4c-a27bca4e86db\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "140f553f-ec59-485c-8476-0694de9241f7", + "metadata": {}, + "outputs": [], + "source": [ + "worker.client.project_client.documents.create(new_doc)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "864a1e90-7660-40fc-8d76-da8646b4ff67", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "capella2polarion", + "language": "python", + "name": "capella2polarion" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/jupyter-notebooks/document_generation.ipynb.license b/jupyter-notebooks/document_generation.ipynb.license new file mode 100644 index 0000000..02c8c23 --- /dev/null +++ b/jupyter-notebooks/document_generation.ipynb.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/jupyter-notebooks/document_templates/test-classes.html.j2 b/jupyter-notebooks/document_templates/test-classes.html.j2 new file mode 100644 index 0000000..977fb23 --- /dev/null +++ b/jupyter-notebooks/document_templates/test-classes.html.j2 @@ -0,0 +1,32 @@ +{# + Copyright DB InfraGO AG and contributors + SPDX-License-Identifier: Apache-2.0 +#} + +{% macro add_class_dependencies(cls, classes) %} + {% if not cls in classes %} + {% set _none = classes.append(cls) %} + {% if cls.super %} + {{ add_class_dependencies(cls.super, classes) }} + {% endif %} + {% for property in cls.properties %} + {% set type = None %} + {% if "type" in property.__dir__() %} + {% set type = property.type %} + {% elif "abstract_type" in property.__dir__() %} + {% set type = property.abstract_type %} + {% endif %} + {% if type and type.xtype == "org.polarsys.capella.core.data.information:Class" %} + {{ add_class_dependencies(type, classes) }} + {% endif %} + {% endfor %} + {% endif %} +{% endmacro %} +{% set cls = model.by_uuid(cls) %} +{% set classes = [] %} +{{ heading(1, "Class Document", session)}} +{{ add_class_dependencies(cls, classes) }} +{{ heading(2, "Data Classes", session)}} +{% for cl in classes | unique %} +{{ insert_work_item(cl, session) }} +{% endfor %} diff --git a/jupyter-notebooks/document_templates/test-icd.html.j2 b/jupyter-notebooks/document_templates/test-icd.html.j2 new file mode 100644 index 0000000..94bb5c8 --- /dev/null +++ b/jupyter-notebooks/document_templates/test-icd.html.j2 @@ -0,0 +1,43 @@ +{# + Copyright DB InfraGO AG and contributors + SPDX-License-Identifier: Apache-2.0 +#} + +{% macro add_class_dependencies(cls, classes) %} + {% if not cls in classes %} + {% set _none = classes.append(cls) %} + {% if cls.super %} + {{ add_class_dependencies(cls.super, classes) }} + {% endif %} + {% for property in cls.properties %} + {% set type = None %} + {% if "type" in property.__dir__() %} + {% set type = property.type %} + {% elif "abstract_type" in property.__dir__() %} + {% set type = property.abstract_type %} + {% endif %} + {% if type and type.xtype == "org.polarsys.capella.core.data.information:Class" %} + {{ add_class_dependencies(type, classes) }} + {% endif %} + {% endfor %} + {% endif %} +{% endmacro %} +{% set interface = model.by_uuid(interface) %} +{{ heading(1, "Interface " + interface.name, session)}} +{{ heading(2, "Interface Partners", session)}} +Source +{{ insert_work_item(interface.source.owner, session) }} +Target +{{ insert_work_item(interface.target.owner, session) }} +{{ heading(2, "Exchange Items", session)}} +{%- set classes = [] %} +{% for ei in interface.exchange_items | unique %} +{{ insert_work_item(ei, session) }} +{% for el in ei.elements %} +{{ add_class_dependencies(el.abstract_type, classes) }} +{% endfor %} +{% endfor %} +{{ heading(2, "Data Classes", session)}} +{% for cl in classes | unique %} +{{ insert_work_item(cl, session) }} +{% endfor %} diff --git a/jupyter-notebooks/element_generation.ipynb b/jupyter-notebooks/element_generation.ipynb new file mode 100644 index 0000000..f504488 --- /dev/null +++ b/jupyter-notebooks/element_generation.ipynb @@ -0,0 +1,182 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 14, + "id": "fc5b3e46-a26a-4a9f-82ea-fcf7e1385f56", + "metadata": {}, + "outputs": [], + "source": [ + "from capella2polarion.connectors import polarion_worker\n", + "from capella2polarion.converters import element_converter, converter_config, data_session\n", + "import dotenv\n", + "import os\n", + "import capellambse" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c3fbe043-1a16-48b1-8cf9-6e549643263a", + "metadata": {}, + "outputs": [], + "source": [ + "dotenv.load_dotenv()\n", + "model = capellambse.MelodyModel(os.environ.get(\"MODEL_PATH\"))\n", + "worker = polarion_worker.CapellaPolarionWorker(\n", + " polarion_worker.PolarionWorkerParams(\n", + " os.environ.get(\"POLARION_PROJECT\"),\n", + " os.environ.get(\"POLARION_HOST\"),\n", + " os.environ.get(\"POLARION_PAT\"),\n", + " False,\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2acf388d-7b12-4056-826c-a7dedad9f78d", + "metadata": {}, + "outputs": [], + "source": [ + "worker.load_polarion_work_item_map()" + ] + }, + { + "cell_type": "markdown", + "id": "ea8d40bd-73c4-4df6-8e0f-9d9b30a40c4f", + "metadata": {}, + "source": [ + "In this example we want to test a jinja template for classes. We want to adjust nothing but the description field and we want to update all classes related to the class, too." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "d29ee234-0ec7-4a2b-a2a6-05cdb170ce67", + "metadata": {}, + "outputs": [], + "source": [ + "def add_class_incl_dependencies(cls, classes):\n", + " if cls in classes:\n", + " return\n", + " classes.append(cls)\n", + " if cls.super:\n", + " add_class_incl_dependencies(cls.super, classes)\n", + " for property in cls.properties:\n", + " _type = None\n", + " if \"type\" in property.__dir__():\n", + " _type = property.type\n", + " elif \"abstract_type\" in property.__dir__():\n", + " _type = property.abstract_type\n", + " if _type and _type.xtype == \"org.polarsys.capella.core.data.information:Class\":\n", + " add_class_incl_dependencies(_type, classes)" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "5dcb5313-aca5-4ffa-88bb-f2644631bcfa", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "36" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "classes = []\n", + "add_class_incl_dependencies(model.by_uuid(\"3e92ba92-89b1-4c4a-abaf-fa78fe95142e\"), classes)\n", + "len(classes)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "9059c90f-58aa-4bea-a472-c51b1c94da35", + "metadata": {}, + "outputs": [], + "source": [ + "class_config = converter_config.CapellaTypeConfig(\n", + " \"class\",\n", + " {\n", + " \"jinja_as_description\": {\n", + " \"template_folder\": \"element_templates\",\n", + " \"template_path\": \"class.html.j2\",\n", + " }\n", + " },\n", + " [],\n", + ")\n", + "serializer = element_converter.CapellaWorkItemSerializer(\n", + " model,\n", + " worker.polarion_data_repo,\n", + " {\n", + " cls.uuid: data_session.ConverterData(\n", + " \"pa\",\n", + " class_config,\n", + " cls,\n", + " )\n", + " for cls in classes\n", + " },\n", + " False,\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "799e5c84-d48f-406c-ab11-356400a9d7a3", + "metadata": {}, + "outputs": [], + "source": [ + "wis=serializer.serialize_all()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6fc38c3b-86dc-4ba8-9ab1-d38b097df768", + "metadata": {}, + "outputs": [], + "source": [ + "worker.client.project_client.work_items.update(wis)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f57fa03-70ba-4dad-b347-628cee9b4425", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "capella2polarion", + "language": "python", + "name": "capella2polarion" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/jupyter-notebooks/element_generation.ipynb.license b/jupyter-notebooks/element_generation.ipynb.license new file mode 100644 index 0000000..02c8c23 --- /dev/null +++ b/jupyter-notebooks/element_generation.ipynb.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/jupyter-notebooks/element_templates/class.html.j2 b/jupyter-notebooks/element_templates/class.html.j2 new file mode 100644 index 0000000..8563c02 --- /dev/null +++ b/jupyter-notebooks/element_templates/class.html.j2 @@ -0,0 +1,82 @@ +{# + Copyright DB InfraGO AG and contributors + SPDX-License-Identifier: Apache-2.0 +#} + +{% from 'common_macros.html.j2' import show_other_attributes, linked_name, linked_name_with_icon, description, display_property_label %} +{% set table_attributes='class="polarion-Document-table" style="margin: auto;margin-left: 0px;empty-cells: show;border-collapse: collapse;max-width: 1280px;border: 1px solid #CCCCCC;" id="polarion_wiki macro name=table"' %} +{% set th_attributes='style="height: 12px;text-align: left;vertical-align: top;font-weight: bold;background-color: #F0F0F0;border: 1px solid #CCCCCC;padding: 5px;"' %} +{% set td_attributes='style="height: 12px;text-align: left;vertical-align: top;line-height: 18px;border: 1px solid #CCCCCC;padding: 5px;"' %} + +{% macro properties_list(props, obj) %} + {% for property in props %} + {{ display_property_label(obj, property) | safe }} + {% set prop_props = [] %} + {% if property.kind != "UNSET" %}{% set _none = prop_props.append(["Kind", property.kind]) %}{% endif %} + {% if property.min_value %}{% set _none = prop_props.append(["Min. value", property.min_value])%}{% endif %} + {% if property.max_value %}{% set _none = prop_props.append(["Max. value", property.max_value])%}{% endif %} +{{property.type.name}} enumeration values:
+Enumeration Literal | +Value | +
---|---|
{{ val.name }} | +{{ val.value if val.value else "0" }} | +
Property {{ property.name }} has the following additional attributes:
+ {% if prop_props %} +Property | +Value | +
---|---|
{{ key }} | +{{ val.value }}{% if val.type is defined %} :{{ linked_name_with_icon(val.type) | safe}}{% endif %} | +
Parent: {{ object.parent.name }}
+{% if object.description %} +{{ object.description }}
+{% else %} +No description available.
+{% endif %} + +{% set props = [] %} +{% if object.super %} + {% set props = props | list + object.super.properties | list %} +{% endif %} +{% set props = props + object.owned_properties | list %} + +Properties +The object owns the properties listed below; We use the following format to describe property: name : type [min .. max (instances of type)] or [ fixed number]; if no multiplicity is shown assume its 1 (single instance).
+ {{ properties_list(props, object) | safe }} +{% else %} +No properties are owned by this object.
+{% endif %} diff --git a/tests/data/jinja_templates/common_macros.html.j2 b/jupyter-notebooks/element_templates/common_macros.html.j2 similarity index 100% rename from tests/data/jinja_templates/common_macros.html.j2 rename to jupyter-notebooks/element_templates/common_macros.html.j2 diff --git a/tests/data/jinja_templates/exchange_item.html.j2 b/jupyter-notebooks/element_templates/exchange_item.html.j2 similarity index 100% rename from tests/data/jinja_templates/exchange_item.html.j2 rename to jupyter-notebooks/element_templates/exchange_item.html.j2 diff --git a/tests/conftest.py b/tests/conftest.py index 502b2c3..99af6f8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -173,3 +173,16 @@ def write(self, text: str): pw = polarion_worker.CapellaPolarionWorker(c2p_cli.polarion_params) pw.polarion_data_repo = polarion_repo.PolarionDataRepository([work_item]) return BaseObjectContainer(c2p_cli, pw, mc) + + +@pytest.fixture +def empty_polarion_worker(monkeypatch: pytest.MonkeyPatch): + mock_api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) + monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api) + polarion_params = polarion_worker.PolarionWorkerParams( + project_id="project_id", + url=TEST_HOST, + pat="PrivateAccessToken", + delete_work_items=True, + ) + yield polarion_worker.CapellaPolarionWorker(polarion_params) diff --git a/tests/data/jinja_templates/class.html.j2 b/tests/data/jinja_templates/class.html.j2 deleted file mode 100644 index 0b8ac1e..0000000 --- a/tests/data/jinja_templates/class.html.j2 +++ /dev/null @@ -1,41 +0,0 @@ -{# - Copyright DB InfraGO AG and contributors - SPDX-License-Identifier: Apache-2.0 -#} - -{% from 'common_macros.html.j2' import show_other_attributes, linked_name, linked_name_with_icon, description, display_property_label %} - -{% macro properties_table(props, obj) %} - {% for property in props %} - {{ display_property_label(obj, property) | safe }} -KIND: {{ property.kind }}
- {% endif %} - {{ description(property) | safe}} -Parent: {{ object.parent.name }}
-{% if object.description %} -{{ object.description }}
-{% else %} -No description available.
-{% endif %} - -{% set props = [] %} -{% if object.super %} - {% set props = props | list + object.super.properties | list %} -{% endif %} -{% set props = props + object.owned_properties | list %} - -Properties -The object owns the properties listed below; We use the following format to describe property: name : type [min .. max (instances of type)] or [ fixed number]; if no multiplicity is shown assume its 1 (single instance).
- {{ properties_table(props, object) | safe }} -{% else %} -No properties are owned by this object.
-{% endif %} diff --git a/tests/data/jinja_templates/functional_exchange.html.j2 b/tests/data/jinja_templates/functional_exchange.html.j2 deleted file mode 100644 index 82e91cb..0000000 --- a/tests/data/jinja_templates/functional_exchange.html.j2 +++ /dev/null @@ -1,24 +0,0 @@ -{# - Copyright DB InfraGO AG and contributors - SPDX-License-Identifier: Apache-2.0 -#} - -{% from 'common_macros.html.j2' import show_other_attributes, description, typed_name, linked_name, linked_name_with_icon, display_property_label %} - -{%- set source_function = object.source.owner -%} -{%- set target_function = object.target.owner -%} -{%- set source = source_function.owner -%} -{%- set target = target_function.owner -%} -The {{ linked_name(source) | safe }} shall provide {{ linked_name_with_icon(object) | safe }} to {{ linked_name_with_icon(target) | safe }} so that the {{ linked_name_with_icon(target) | safe }} could {{ linked_name_with_icon(target_function)|safe }}. -{% if object.exchange_items %} -{% if object.exchange_items | length > 1 %} -{{ object.name }} is further specified via the following Exchange Items:
-This interaction is further specified via {{ linked_name_with_icon(object.exchange_items[0]) | safe}} Exchange Item
-{% endif %} -{% endif %} diff --git a/tests/test_documents.py b/tests/test_documents.py index dd5d085..fde5295 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -1,2 +1,137 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 +import capellambse +import polarion_rest_api_client as polarion_api +from lxml import etree, html + +from capella2polarion import data_models as dm +from capella2polarion.connectors import polarion_worker +from capella2polarion.converters import document_renderer + + +def test_create_new_document( + empty_polarion_worker: polarion_worker.CapellaPolarionWorker, + model: capellambse.MelodyModel, +): + empty_polarion_worker.polarion_data_repo.update_work_items( + [ + dm.CapellaWorkItem( + "ATSY-1234", + uuid_capella="c710f1c2-ede6-444e-9e2b-0ff30d7fd040", + type="class", + ), + dm.CapellaWorkItem( + "ATSY-4321", + uuid_capella="2b34c799-769c-42f2-8a1b-4533dba209a0", + type="class", + ), + ] + ) + + renderer = document_renderer.DocumentRenderer( + empty_polarion_worker.polarion_data_repo, model + ) + + new_doc, wis = renderer.render_document( + "jupyter-notebooks/document_templates", + "test-classes.html.j2", + "_default", + "TEST-DOC", + cls="c710f1c2-ede6-444e-9e2b-0ff30d7fd040", + ) + + content = html.fromstring(new_doc.home_page_content.value) + + assert new_doc.rendering_layouts == [ + polarion_api.RenderingLayout( + label="Class", type="class", layouter="section" + ) + ] + assert len(content) == 4 + assert ( + etree.tostring(content[0]) + .decode("utf-8") + .startswith("