From 8b9a076d8609d8f5bb27dcf506c99dd6f7927514 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 21 May 2026 06:57:06 +0300 Subject: [PATCH] Load EDT form elements on demand --- .../src/one_c_normalizer/__init__.py | 68 ++++++++++- .../tests/test_xml_indexing.py | 32 +++++ services/api-server/src/api_server/main.py | 113 +++++++++++++++++- services/api-server/tests/test_api.py | 5 +- 4 files changed, 209 insertions(+), 9 deletions(-) diff --git a/packages/one-c-normalizer/src/one_c_normalizer/__init__.py b/packages/one-c-normalizer/src/one_c_normalizer/__init__.py index 725d18b..ab91e3a 100644 --- a/packages/one-c-normalizer/src/one_c_normalizer/__init__.py +++ b/packages/one-c-normalizer/src/one_c_normalizer/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations import hashlib -from pathlib import Path +from pathlib import Path, PurePosixPath import re import xml.etree.ElementTree as ET @@ -362,13 +362,13 @@ def _walk_xml_objects( if right is not None: result.append(right) elif object_kind is not None: - name = _xml_name(element) + name = _xml_name(element, source_path=source_path) if name: xml_object = OneCXmlObject( source_path=source_path, object_kind=object_kind, name=name, - qualified_name=_xml_qualified_name(element, name, object_kind, parent_qualified_name), + qualified_name=_xml_qualified_name(element, name, object_kind, parent_qualified_name, source_path), attributes=_xml_attributes(element), ) result.append(xml_object) @@ -1332,7 +1332,7 @@ def _xml_type_name(element: ET.Element) -> str: return "" -def _xml_name(element: ET.Element) -> str: +def _xml_name(element: ET.Element, *, source_path: str = "") -> str: for key in ("name", "Name", "Имя"): if key in element.attrib: return element.attrib[key] @@ -1340,6 +1340,9 @@ def _xml_name(element: ET.Element) -> str: if _local_name(child.tag).lower() in {"name", "имя"} and child.text: return child.text.strip() tag = _local_name(element.tag).lower() + edt_form = _edt_form_context_from_path(source_path) + if tag == "form" and edt_form is not None: + return edt_form[0] fallback_keys = { "urltemplate": ("template", "url", "path", "Шаблон", "URL"), "urltemplates": ("template", "url", "path", "Шаблон", "URL"), @@ -1388,6 +1391,7 @@ def _xml_qualified_name( name: str, object_kind: str, parent_qualified_name: str | None, + source_path: str = "", ) -> str: for key in ("qualifiedName", "QualifiedName", "ПолноеИмя"): if key in element.attrib: @@ -1395,6 +1399,10 @@ def _xml_qualified_name( for child in _xml_property_children(element): if _local_name(child.tag).lower() in {"qualifiedname", "полноеимя"} and child.text: return child.text.strip() + if object_kind == "FORM" and parent_qualified_name is None: + edt_form = _edt_form_context_from_path(source_path) + if edt_form is not None: + return edt_form[1] if parent_qualified_name: if object_kind in _ROOT_METADATA_OBJECT_KINDS and object_kind not in {"PROJECT", "ROLE"}: prefix = _QUALIFIED_PREFIX_BY_KIND.get(object_kind, object_kind) @@ -1407,6 +1415,48 @@ def _xml_qualified_name( return name +_EDT_OWNER_PREFIX_BY_DIRECTORY = { + "AccountingRegisters": "РегистрБухгалтерии", + "AccumulationRegisters": "РегистрНакопления", + "BusinessProcesses": "БизнесПроцесс", + "CalculationRegisters": "РегистрРасчета", + "Catalogs": "Справочник", + "ChartsOfAccounts": "ПланСчетов", + "ChartsOfCalculationTypes": "ПланВидовРасчета", + "ChartsOfCharacteristicTypes": "ПланВидовХарактеристик", + "DataProcessors": "Обработка", + "DocumentJournals": "ЖурналДокументов", + "Documents": "Документ", + "Enums": "Перечисление", + "ExchangePlans": "ПланОбмена", + "ExternalDataSources": "ВнешнийИсточникДанных", + "InformationRegisters": "РегистрСведений", + "Reports": "Отчет", + "Tasks": "Задача", +} + + +def _edt_form_context_from_path(source_path: str) -> tuple[str, str] | None: + if not source_path or PurePosixPath(source_path).name.casefold() != "form.form": + return None + parts = PurePosixPath(source_path).parts + try: + forms_index = parts.index("Forms") + except ValueError: + forms_index = -1 + if forms_index > 1 and forms_index + 1 < len(parts): + owner_directory = parts[forms_index - 2] + owner_name = parts[forms_index - 1] + form_name = parts[forms_index + 1] + owner_prefix = _EDT_OWNER_PREFIX_BY_DIRECTORY.get(owner_directory) + if owner_prefix: + return form_name, f"{owner_prefix}.{owner_name}.{form_name}" + if len(parts) >= 3 and parts[-3] == "CommonForms": + form_name = parts[-2] + return form_name, f"ОбщаяФорма.{form_name}" + return None + + def _xml_attributes(element: ET.Element) -> dict: attributes = dict(element.attrib) for key, value in element.attrib.items(): @@ -1445,10 +1495,18 @@ def _xml_nested_text_value(element: ET.Element) -> str: return localized.get("ru") or localized.get("ru_RU") or next(iter(localized.values())) if _local_name(element.tag).lower() == "value": return _element_text_content(element) + path_segments = [ + _element_text_content(child) + for child in element + if _local_name(child.tag).lower() in {"segment", "segments", "pathsegment"} + ] + path_segments = [value for value in path_segments if value] + if path_segments: + return ".".join(path_segments) values = [ _element_text_content(child) for child in element - if _local_name(child.tag).lower() in {"value", "text", "строка", "представление"} + if _local_name(child.tag).lower() in {"value", "text", "строка", "представление", "caption"} ] values = [value for value in values if value] if values: diff --git a/packages/semantic-kernel/tests/test_xml_indexing.py b/packages/semantic-kernel/tests/test_xml_indexing.py index 17b08d1..5b2f458 100644 --- a/packages/semantic-kernel/tests/test_xml_indexing.py +++ b/packages/semantic-kernel/tests/test_xml_indexing.py @@ -373,6 +373,38 @@ def test_index_project_extracts_managed_form_items_without_layouts(tmp_path: Pat assert not any(element.name == "ПечатнаяФорма" for element in form.elements) +def test_index_project_extracts_edt_form_items_from_form_file_path(tmp_path: Path): + form_dir = tmp_path / "src" / "Catalogs" / "ВидыЗаказовПокупателей" / "Forms" / "ФормаСписка" + form_dir.mkdir(parents=True) + (form_dir / "Form.form").write_text( + """ + + + Список + + Список + + + Наименование + + Список.Description + + + + +""", + encoding="utf-8", + ) + + snapshot = index_project(tmp_path, project_id="edt-form-items") + + form = next(item for item in form_semantics(snapshot) if item.form.name == "ФормаСписка") + assert form.form.qualified_name == "Справочник.ВидыЗаказовПокупателей.ФормаСписка" + assert [element.name for element in form.elements] == ["Список", "Наименование"] + assert form.elements[0].attributes["control_kind"] == "Table" + assert form.elements[1].attributes["dataPath"] == "Список.Description" + + def test_index_project_links_form_events_to_handlers(tmp_path: Path): xml = tmp_path / "form.xml" xml.write_text( diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index 237bd05..a3709a0 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -227,8 +227,9 @@ from semantic_versioning import ( SemanticObjectVersion, diff_versions, ) +from one_c_normalizer import parse_one_c_xml_file from semantic_kernel import index_project -from sir import DiagnosticSeverity, EdgeKind, NodeKind, SirDelta, SirSnapshot, stable_hash +from sir import DiagnosticSeverity, EdgeKind, NodeKind, SirDelta, SirSnapshot, make_lineage_id, stable_hash from storage_core import FileStorage, StoredSnapshotInfo from transaction_topology import routines_touching_target, transaction_write_sets from ui_semantics import form_semantics @@ -4755,7 +4756,11 @@ async def get_object_ui(project_id: str, object_name: str) -> ObjectUiResponse: } return ObjectUiResponse( object=_named_node(object_node), - forms=_form_semantics_for_lineages(snapshot, form_lineages), + forms=_hydrate_object_ui_forms_from_source( + project_id, + object_node, + _form_semantics_for_lineages(snapshot, form_lineages), + ), ) @@ -7884,6 +7889,110 @@ def _form_semantics_for_lineages(snapshot: SirSnapshot, form_lineages: set[str]) ] +_EDT_SOURCE_DIRECTORY_BY_PREFIX = { + "РегистрБухгалтерии": "AccountingRegisters", + "РегистрНакопления": "AccumulationRegisters", + "БизнесПроцесс": "BusinessProcesses", + "РегистрРасчета": "CalculationRegisters", + "Справочник": "Catalogs", + "ПланСчетов": "ChartsOfAccounts", + "ПланВидовРасчета": "ChartsOfCalculationTypes", + "ПланВидовХарактеристик": "ChartsOfCharacteristicTypes", + "Обработка": "DataProcessors", + "ЖурналДокументов": "DocumentJournals", + "Документ": "Documents", + "Перечисление": "Enums", + "ПланОбмена": "ExchangePlans", + "ВнешнийИсточникДанных": "ExternalDataSources", + "РегистрСведений": "InformationRegisters", + "Отчет": "Reports", + "Задача": "Tasks", +} + + +def _hydrate_object_ui_forms_from_source( + project_id: str, + object_node, + forms: list[FormSemanticsResponse], +) -> list[FormSemanticsResponse]: + source_root = _current_project_source_root(project_id) + if source_root is None: + return forms + return [ + _hydrate_form_elements_from_source(project_id, source_root, object_node, form) + for form in forms + ] + + +def _hydrate_form_elements_from_source( + project_id: str, + source_root: Path, + object_node, + form: FormSemanticsResponse, +) -> FormSemanticsResponse: + if form.elements: + return form + form_file = _edt_form_file_for_object(source_root, object_node.qualified_name, form.form.name) + if form_file is None: + return form + try: + xml_objects = parse_one_c_xml_file(form_file) + except (OSError, UnicodeDecodeError, ET.ParseError): + return form + source_path = form_file.as_posix() + elements = [ + NamedNode( + lineage_id=make_lineage_id( + NodeKind.FORM_ELEMENT.value, + f"{project_id}:{source_path}:ELEMENT:{item.qualified_name}", + ), + kind=NodeKind.FORM_ELEMENT.value, + name=item.name, + qualified_name=item.qualified_name, + attributes=item.attributes, + ) + for item in xml_objects + if item.object_kind == "ELEMENT" and item.qualified_name.startswith(f"{form.form.qualified_name}.") + ] + if not elements: + return form + return FormSemanticsResponse( + form=form.form, + commands=form.commands, + elements=elements, + command_handlers=form.command_handlers, + ) + + +def _current_project_source_root(project_id: str) -> Path | None: + state = _project_setup.get(project_id, {}) + last_import = state.get("last_import") or {} + source_path = last_import.get("source_path") + if not source_path: + return None + root = Path(source_path) + return root if root.exists() else None + + +def _edt_form_file_for_object(source_root: Path, object_qualified_name: str, form_name: str) -> Path | None: + parts = object_qualified_name.split(".") + if len(parts) < 2: + return None + prefix, object_name = parts[0], parts[1] + source_dirs = [source_root / "src", *(path / "src" for path in source_root.iterdir() if path.is_dir())] + if prefix == "ОбщаяФорма": + candidates = [source_dir / "CommonForms" / object_name / "Form.form" for source_dir in source_dirs] + else: + directory = _EDT_SOURCE_DIRECTORY_BY_PREFIX.get(prefix) + if directory is None: + return None + candidates = [ + source_dir / directory / object_name / "Forms" / form_name / "Form.form" + for source_dir in source_dirs + ] + return next((candidate for candidate in candidates if candidate.exists()), None) + + def _persist_job(job: OperationJob) -> OperationJob: _storage.write_document("operations_jobs", job.job_id, job.model_dump(mode="json")) return job diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index c212b68..5f33942 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -919,8 +919,9 @@ def test_html5_project_setup_renders_server_fragments(): reindex = client.post(f"/html5/projects/{project_id}/setup/reindex") assert reindex.status_code == 200 - assert "data-html5-setup-summary" in reindex.text - assert "reindexed" in reindex.text + assert "data-html5-import-job" in reindex.text + assert "Переиндексация" in reindex.text + assert 'sse-swap="setup-import-job"' in reindex.text assert "