Extract managed form elements from XML
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-21 06:10:05 +03:00
parent 5bd188fe6f
commit af900e4e34
9 changed files with 186 additions and 46 deletions
@@ -171,7 +171,7 @@ def parse_one_c_xml_file(path: str | Path) -> list[OneCXmlObject]:
source_path = normalize_source_path(path)
root = ET.fromstring(_read_text_file(Path(path)))
result: list[OneCXmlObject] = []
_walk_xml_objects(source_path, root, result, current_role=None, parent_qualified_name=None)
_walk_xml_objects(source_path, root, result, current_role=None, parent_qualified_name=None, parent_object_kind=None)
return result
@@ -351,10 +351,12 @@ def _walk_xml_objects(
*,
current_role: OneCXmlObject | None,
parent_qualified_name: str | None,
parent_object_kind: str | None,
) -> None:
role_context = current_role
child_parent_qualified_name = parent_qualified_name
object_kind = _xml_object_kind(element)
child_parent_object_kind = parent_object_kind
object_kind = _xml_object_kind(element, parent_object_kind=parent_object_kind)
if object_kind == "RIGHT":
right = _xml_right_object(source_path, element, role_context)
if right is not None:
@@ -371,6 +373,7 @@ def _walk_xml_objects(
)
result.append(xml_object)
child_parent_qualified_name = xml_object.qualified_name
child_parent_object_kind = object_kind
if object_kind == "ROLE":
role_context = xml_object
@@ -381,6 +384,7 @@ def _walk_xml_objects(
result,
current_role=role_context,
parent_qualified_name=child_parent_qualified_name,
parent_object_kind=child_parent_object_kind,
)
@@ -695,6 +699,25 @@ _OBJECT_KIND_BY_TAG = {
"предопределенный": "PREDEFINED",
}
_FORM_ELEMENT_TAGS = {
"item",
"items",
"element",
"elements",
"formitem",
"formitems",
"field",
"fields",
"group",
"groups",
"table",
"tables",
"button",
"buttons",
"page",
"pages",
}
_QUALIFIED_PREFIX_BY_KIND = {
"CATALOG": "Справочник",
@@ -1288,8 +1311,10 @@ _PATH_METADATA_ALIASES = {
}
def _xml_object_kind(element: ET.Element) -> str | None:
def _xml_object_kind(element: ET.Element, *, parent_object_kind: str | None = None) -> str | None:
tag = _local_name(element.tag).lower()
if parent_object_kind in {"FORM", "ELEMENT"} and tag in _FORM_ELEMENT_TAGS and _xml_name(element):
return "ELEMENT"
if tag in {"metadataobject", "object"}:
type_name = _xml_type_name(element)
if type_name:
@@ -1384,6 +1409,11 @@ def _xml_qualified_name(
def _xml_attributes(element: ET.Element) -> dict:
attributes = dict(element.attrib)
for key, value in element.attrib.items():
local_key = _local_name(key)
attributes.setdefault(local_key, value)
if local_key.lower() == "type":
attributes.setdefault("control_kind", value.split(":")[-1].split(".")[-1])
attribute_role = _xml_attribute_role(element)
if attribute_role:
attributes.setdefault("attribute_role", attribute_role)
@@ -983,7 +983,9 @@ def _xml_edge_kind(kind: NodeKind) -> EdgeKind:
return EdgeKind.HAS_TABULAR_SECTION
if kind == NodeKind.ROLE:
return EdgeKind.HAS_ROLE
return EdgeKind.HAS_ELEMENT
if kind == NodeKind.FORM_ELEMENT:
return EdgeKind.HAS_ELEMENT
return EdgeKind.CONTAINS
def _find_xml_parent(parents: dict[str, SemanticNode], qualified_name: str) -> SemanticNode | None:
@@ -346,6 +346,33 @@ def test_index_project_links_form_command_to_handler(tmp_path: Path):
assert any(edge.kind == EdgeKind.HANDLES for edge in snapshot.edges)
def test_index_project_extracts_managed_form_items_without_layouts(tmp_path: Path):
xml = tmp_path / "form.xml"
xml.write_text(
"""
<Form name="ФормаДокумента" qualifiedName="Документ.Заказ.ФормаДокумента">
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="form:FormGroup" name="Основное">
<caption>Основное</caption>
<items xsi:type="form:FormField" name="Номер">
<caption>Номер</caption>
<dataPath>Объект.Номер</dataPath>
</items>
</items>
<Layout name="ПечатнаяФорма" qualifiedName="Документ.Заказ.ФормаДокумента.ПечатнаяФорма" />
</Form>
""",
encoding="utf-8",
)
snapshot = index_project(tmp_path, project_id="ui-form-items")
form = form_semantics(snapshot)[0]
assert [element.name for element in form.elements] == ["Основное", "Номер"]
assert all(element.kind == NodeKind.FORM_ELEMENT for element in form.elements)
assert form.elements[1].attributes["dataPath"] == "Объект.Номер"
assert not any(element.name == "ПечатнаяФорма" for element in form.elements)
def test_index_project_links_form_events_to_handlers(tmp_path: Path):
xml = tmp_path / "form.xml"
xml.write_text(
@@ -19,15 +19,20 @@ def form_semantics(snapshot: SirSnapshot) -> list[FormSemantics]:
for node in snapshot.nodes
if node.kind == NodeKind.FORM
}
element_children: dict[str, list[SemanticNode]] = {}
for edge in snapshot.edges:
form = forms.get(edge.source_lineage)
target = nodes.get(edge.target_lineage)
if form is None or target is None:
if target is None:
continue
if edge.kind == EdgeKind.HAS_ELEMENT and target.kind == NodeKind.FORM_ELEMENT:
element_children.setdefault(edge.source_lineage, []).append(target)
if form is None:
continue
if edge.kind == EdgeKind.HAS_COMMAND:
form.commands.append(target)
elif edge.kind == EdgeKind.HAS_ELEMENT:
form.elements.append(target)
for form in forms.values():
form.elements.extend(_flatten_form_elements(form.form.lineage_id, element_children))
command_to_form = {
command.lineage_id: form
for form in forms.values()
@@ -43,4 +48,20 @@ def form_semantics(snapshot: SirSnapshot) -> list[FormSemantics]:
return sorted(forms.values(), key=lambda item: item.form.qualified_name)
def _flatten_form_elements(root_lineage: str, element_children: dict[str, list[SemanticNode]]) -> list[SemanticNode]:
result: list[SemanticNode] = []
seen: set[str] = set()
def visit(parent_lineage: str) -> None:
for element in element_children.get(parent_lineage, []):
if element.lineage_id in seen:
continue
seen.add(element.lineage_id)
result.append(element)
visit(element.lineage_id)
visit(root_lineage)
return result
__all__ = ["FormSemantics", "form_semantics"]