Skip to content

Commit

Permalink
feat: Implement document redering and add some jupyter notebooks for …
Browse files Browse the repository at this point in the history
…a fast jinja template development
  • Loading branch information
micha91 committed Jul 3, 2024
1 parent 167e921 commit b3330a4
Show file tree
Hide file tree
Showing 19 changed files with 995 additions and 128 deletions.
6 changes: 5 additions & 1 deletion capella2polarion/connectors/polarion_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ 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(
"HAS_VALUE:uuid_capella",
{"workitems": "id,uuid_capella,checksum,status"},
{"workitems": "id,uuid_capella,checksum,status,type"},
)
self.polarion_data_repo.update_work_items(work_items)

Expand Down Expand Up @@ -419,3 +419,7 @@ def patch_work_items(
for uuid, data in converter_session.items():
if uuid in self.polarion_data_repo and data.work_item is not None:
self.patch_work_item(data)

def post_document(self, document: polarion_api.Document):
"""Create a new Document."""
self.client.project_client.documents.create(document)
187 changes: 187 additions & 0 deletions capella2polarion/converters/document_renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
"""A jinja renderer for Polarion documents."""

import dataclasses
import pathlib
import re
import typing as t

import capellambse
import jinja2
import polarion_rest_api_client as polarion_api
from capellambse import helpers as chelpers
from lxml import etree

from capella2polarion.connectors import polarion_repo

from . import polarion_html_helper

heading_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}(.*)")


@dataclasses.dataclass
class RenderingSession:
"""A data class for parameters handled during a rendering session."""

headings: list[polarion_api.WorkItem] = dataclasses.field(
default_factory=list
)
heading_ids: list[str] = dataclasses.field(default_factory=list)
rendering_layouts: list[polarion_api.RenderingLayout] = dataclasses.field(
default_factory=list
)


class DocumentRenderer(polarion_html_helper.JinjaRendererMixin):
"""A Renderer class for Polarion documents."""

def __init__(
self,
polarion_repository: polarion_repo.PolarionDataRepository,
model: capellambse.MelodyModel,
):
self.polarion_repository = polarion_repository
self.model = model
self.jinja_envs: dict[str, jinja2.Environment] = {}

def setup_env(self, env: jinja2.Environment):
"""Add globals and filters to the environment."""
env.globals["insert_work_item"] = self.__insert_work_item
env.globals["heading"] = self.__heading
env.filters["link_work_item"] = self.__link_work_item

def __insert_work_item(
self, obj: object, session: RenderingSession
) -> str | None:
if (obj := self.check_model_element(obj)) is None:
raise TypeError("object passed was no model element")

if wi := self.polarion_repository.get_work_item_by_capella_uuid(
obj.uuid
):
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
),
)
)

return polarion_html_helper.POLARION_WORK_ITEM_DOCUMENT.format(
pid=wi.id, lid=layout_index
)

return polarion_html_helper.RED_TEXT.format(
text=f"Missing WorkItem for UUID {obj.uuid}"
)

def __link_work_item(self, obj: object) -> str | None:
if (obj := self.check_model_element(obj)) is None:
raise TypeError("object passed was no model element")

if wi := self.polarion_repository.get_work_item_by_capella_uuid(
obj.uuid
):
return polarion_html_helper.POLARION_WORK_ITEM_URL.format(
pid=wi.id
)

return polarion_html_helper.RED_TEXT.format(
text=f"Missing WorkItem for UUID {obj.uuid}"
)

def __heading(self, level: int, text: str, session: RenderingSession):
if session.heading_ids:
hid = session.heading_ids.pop(0)
session.headings.append(polarion_api.WorkItem(id=hid, title=text))
return f'<h{level} id="{heading_id_prefix}{hid}"></h{level}>'
return f"<h{level}>{text}</h{level}>"

@t.overload
def render_document(
self,
template_folder: str | pathlib.Path,
template_name: str,
polarion_folder: str,
polarion_name: str,
**kwargs: t.Any,
):
"""Render a new Polarion document."""

@t.overload
def render_document(
self,
template_folder: str | pathlib.Path,
template_name: str,
*,
document: polarion_api.Document,
**kwargs: t.Any,
):
"""Update an existing Polarion document."""

def render_document(
self,
template_folder: str | pathlib.Path,
template_name: str,
polarion_folder: str | None = None,
polarion_name: str | None = None,
document: polarion_api.Document | None = None,
**kwargs: t.Any,
):
"""Render a Polarion document."""
if document is not None:
polarion_folder = document.module_folder
polarion_name = document.module_name

assert polarion_name is not None and polarion_folder is not None, (
"You either need to pass a folder and a name or a document with a "
"module_folder and a module_name defined"
)

env = self._get_jinja_env(template_folder)
template = env.get_template(template_name)

session = RenderingSession()
if document is not None:
session.rendering_layouts = document.rendering_layouts or []
if document.home_page_content and document.home_page_content.value:
session.heading_ids = self._extract_headings(document)
else:
document = polarion_api.Document(
module_folder=polarion_folder,
module_name=polarion_name,
)

document.home_page_content = polarion_api.TextContent(
"text/html",
template.render(model=self.model, session=session, **kwargs),
)
document.rendering_layouts = session.rendering_layouts

return document, session.headings

def _extract_headings(self, document):
heading_ids = []

def collect_heading_work_items(element: etree._Element):
if h_regex.fullmatch(element.tag):
matches = wi_regex.match(element.get("id"))
if matches:
heading_ids.append(matches.group(1))

_ = chelpers.process_html_fragments(
document.home_page_content.value, collect_heading_work_items
)
return heading_ids
74 changes: 15 additions & 59 deletions capella2polarion/converters/element_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,13 @@

from capella2polarion import data_models
from capella2polarion.connectors import polarion_repo
from capella2polarion.converters import data_session
from capella2polarion.converters import data_session, polarion_html_helper

RE_DESCR_DELETED_PATTERN = re.compile(
f"&lt;deleted element ({chelpers.RE_VALID_UUID.pattern})&gt;"
)
RE_DESCR_LINK_PATTERN = re.compile(
r"<a href=\"hlink://([^\"]+)\">([^<]+)<\/a>"
)
RE_CAMEL_CASE_2ND_WORD_PATTERN = re.compile(r"([a-z]+)([A-Z][a-z]+)")

POLARION_WORK_ITEM_URL = (
'<span class="polarion-rte-link" data-type="workItem" '
'id="fake" data-item-id="{pid}" data-option-id="long">'
"</span>"
)

PrePostConditionElement = t.Union[
oa.OperationalCapability, interaction.Scenario
]
Expand All @@ -54,13 +45,6 @@ def resolve_element_type(type_: str) -> str:
return type_[0].lower() + type_[1:]


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'<span style="text-decoration: line-through;">{string}</span>'


def _format_texts(
type_texts: dict[str, list[str]]
) -> dict[str, dict[str, str]]:
Expand Down Expand Up @@ -88,19 +72,7 @@ def _condition(
}


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'<span><img title="{title}" class="{cls}" '
f'src="workitemimg:{attachment_id}" '
f'style="max-width: {max_width}px;"/></span>'
)
return description


class CapellaWorkItemSerializer:
class CapellaWorkItemSerializer(polarion_html_helper.JinjaRendererMixin):
"""The general serializer class for CapellaWorkItems."""

diagram_cache_path: pathlib.Path
Expand Down Expand Up @@ -226,7 +198,9 @@ def _draw_diagram_svg(
attachment = None

return (
_generate_image_html(title, file_name, max_width, cls),
polarion_html_helper.generate_image_html(
title, file_name, max_width, cls
),
attachment,
)

Expand All @@ -236,41 +210,21 @@ def _render_jinja_template(
template_path: str | pathlib.Path,
model_element: capellambse.model.GenericElement,
):
env = self.__get_jinja_env(str(template_folder))
env = self._get_jinja_env(str(template_folder))
template = env.get_template(template_path)
rendered_jinja = template.render(
object=model_element, model=self.model
)
_, text, _ = self._sanitize_text(model_element, rendered_jinja)
return text

def __get_jinja_env(self, template_folder: str):
if env := self.jinja_envs.get(template_folder):
return env

env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_folder)
)
def setup_env(self, env: jinja2.Environment):
"""Add the link rendering filter."""
env.filters["make_href"] = self.__make_href_filter

self.jinja_envs[template_folder] = env
return env

def __make_href_filter(self, obj: object) -> str | None:
if jinja2.is_undefined(obj) or obj is None:
if (obj := self.check_model_element(obj)) is None:
return "#"

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 f"hlink://{obj.uuid}"

def _draw_additional_attributes_diagram(
Expand Down Expand Up @@ -305,8 +259,8 @@ def _sanitize_linked_text(
linked_text = getattr(
obj, "specification", {"capella:linkedText": markupsafe.Markup("")}
)["capella:linkedText"]
linked_text = RE_DESCR_DELETED_PATTERN.sub(
lambda match: strike_through(
linked_text = polarion_html_helper.RE_DESCR_DELETED_PATTERN.sub(
lambda match: polarion_html_helper.strike_through(
self._replace_markup(obj.uuid, match, [])
),
linked_text,
Expand Down Expand Up @@ -388,10 +342,12 @@ def _replace_markup(
self.converter_session[origin_uuid].errors.add(
f"Non-existing model element referenced in description: {uuid}"
)
return strike_through(match.group(default_group))
return polarion_html_helper.strike_through(
match.group(default_group)
)
if pid := self.capella_polarion_mapping.get_work_item_id(uuid):
referenced_uuids.append(uuid)
return POLARION_WORK_ITEM_URL.format(pid=pid)
return polarion_html_helper.POLARION_WORK_ITEM_URL.format(pid=pid)

self.converter_session[origin_uuid].errors.add(
f"Non-existing work item referenced in description: {uuid}"
Expand Down
9 changes: 7 additions & 2 deletions capella2polarion/converters/link_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from capellambse.model import diagram as diag
from capellambse.model.crosslayer import fa

import capella2polarion.converters.polarion_html_helper
from capella2polarion import data_models
from capella2polarion.connectors import polarion_repo
from capella2polarion.converters import (
Expand Down Expand Up @@ -359,7 +360,9 @@ 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 = element_converter.POLARION_WORK_ITEM_URL.format(pid=link_id)
url = capella2polarion.converters.polarion_html_helper.POLARION_WORK_ITEM_URL.format(
pid=link_id
)
urls.append(f"<li>{url}</li>")
for key, include_wids in link_map[link_id].items():
_, display_name, _ = key.split(":")
Expand All @@ -376,7 +379,9 @@ def _sorted_unordered_html_list(
) -> str:
urls: list[str] = []
for pid in work_item_ids:
url = element_converter.POLARION_WORK_ITEM_URL.format(pid=pid)
url = capella2polarion.converters.polarion_html_helper.POLARION_WORK_ITEM_URL.format(
pid=pid
)
urls.append(f"<li>{url}</li>")

urls.sort()
Expand Down
Loading

0 comments on commit b3330a4

Please sign in to comment.