Load EDT form elements on demand
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
"""
|
||||
<form:Form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:form="http://g5.1c.ru/v8/dt/form">
|
||||
<items xsi:type="form:Table">
|
||||
<name>Список</name>
|
||||
<dataPath xsi:type="form:DataPath">
|
||||
<segments>Список</segments>
|
||||
</dataPath>
|
||||
<items xsi:type="form:FormField">
|
||||
<name>Наименование</name>
|
||||
<dataPath xsi:type="form:DataPath">
|
||||
<segments>Список.Description</segments>
|
||||
</dataPath>
|
||||
</items>
|
||||
</items>
|
||||
</form:Form>
|
||||
""",
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "<html" not in reindex.text
|
||||
|
||||
summary = client.get(f"/html5/projects/{project_id}/setup/summary")
|
||||
|
||||
Reference in New Issue
Block a user