Files

404 lines
15 KiB
Python

"""Semantic profiles for 1C managed form XML exports."""
from __future__ import annotations
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any
FORM_XML_KIND_RU = {
"Form": "Форма",
"Event": "Событие",
"Attribute": "Реквизит формы",
"Column": "Колонка реквизита",
"Command": "Команда формы",
"AutoCommandBar": "Командная панель",
"Button": "Кнопка",
"ButtonGroup": "Группа кнопок",
"CommandBar": "Командная панель",
"Popup": "Подменю",
"Pages": "Страницы",
"ColumnGroup": "Группа колонок",
"LabelField": "Поле надписи",
"InputField": "Поле ввода",
"CheckBoxField": "Поле флажка",
"Table": "Таблица",
"TableColumn": "Колонка таблицы",
"Page": "Страница",
"UsualGroup": "Группа",
"DecorativeLabel": "Декорация надпись",
"ContextMenu": "Контекстное меню",
"ExtendedTooltip": "Расширенная подсказка",
"SearchStringAddition": "Дополнение строки поиска",
"ViewStatusAddition": "Дополнение состояния просмотра",
"SearchControlAddition": "Дополнение управления поиском",
}
FORM_XML_PROPERTY_NAMES = {
"name": "Имя",
"id": "Идентификатор",
"Title": "Заголовок",
"Type": "Вид",
"DataPath": "ПутьКДанным",
"TitleLocation": "ПоложениеЗаголовка",
"Visible": "Видимость",
"UserVisible": "ПользовательскаяВидимость",
"Enabled": "Доступность",
"ReadOnly": "ТолькоПросмотр",
"SkipOnInput": "ПропускатьПриВводе",
"DefaultItem": "АктивизироватьПоУмолчанию",
"Importance": "ВажностьПриОтображении",
"ServerUnavailabilityBehavior": "ПоведениеПриНедоступностиОсновногоСервера",
"PasswordMode": "РежимПароля",
"Hyperlink": "Гиперссылка",
"CommandName": "ИмяКоманды",
"WindowOpeningMode": "РежимОткрытияОкна",
"AutoSaveDataInSettings": "АвтоСохранениеДанныхВНастройках",
"CommandBarLocation": "ПоложениеКоманднойПанели",
"ShowCommandBar": "ОтображатьКоманднуюПанель",
"AutoCommandBar": "АвтоКоманднаяПанель",
"SearchStringAddition": "ДополнениеСтрокиПоиска",
"ViewStatusAddition": "ДополнениеСостоянияПросмотра",
"SearchControlAddition": "ДополнениеУправленияПоиском",
"Representation": "Отображение",
"ChangeRowSet": "ИзменятьСоставСтрок",
"RowSelectionMode": "РежимВыделенияСтроки",
"HorizontalLinesBWA": "ГоризонтальныеЛинии",
"VerticalLinesBWA": "ВертикальныеЛинии",
"UseAlternationRowColorBWA": "ЧередованиеЦветовСтрок",
"AutoInsertNewRow": "АвтоВставкаНовойСтроки",
"EnableStartDrag": "РазрешитьНачалоПеретаскивания",
"EnableDrag": "РазрешитьПеретаскивание",
"RowFilter": "ОтборСтрок",
"HeightInTableRows": "ВысотаВСтрокахТаблицы",
"Footer": "Подвал",
"FileDragMode": "РежимПеретаскиванияФайлов",
"SearchStringLocation": "ПоложениеСтрокиПоиска",
"ViewStatusLocation": "ПоложениеСостоянияПросмотра",
"SearchControlLocation": "ПоложениеУправленияПоиском",
"ButtonGroup": "СоставКоманд",
"Group": "Группировка",
"Behavior": "Поведение",
"ShowTitle": "ПоказыватьЗаголовок",
"Autofill": "Автозаполнение",
"EditWarningRepresentation": "ОтображениеПредупрежденияПриРедактировании",
"EditWarning": "ПредупреждениеПриРедактировании",
"EditMode": "РежимРедактирования",
"AutoEditMode": "АвтоРежимРедактирования",
"AutoCellHeight": "АвтоВысотаЯчейки",
"FixInTable": "ФиксацияВТаблице",
"Shortcut": "СочетаниеКлавиш",
"CommandSet": "СоставКоманд",
"UseCopy": "ИспользоватьКопирование",
"ShowInHeader": "ОтображатьВШапке",
"ShowInFooter": "ОтображатьВПодвале",
"ToolTip": "Подсказка",
"ToolTipRepresentation": "ОтображениеПодсказки",
"DropListButton": "КнопкаВыпадающегоСписка",
"ChoiceButton": "КнопкаВыбора",
"ClearButton": "КнопкаОчистки",
"SpinButton": "КнопкаРегулирования",
"OpenButton": "КнопкаОткрытия",
"CreateButton": "КнопкаСоздания",
"QuickChoice": "БыстрыйВыбор",
"ChooseType": "ВыбиратьТип",
"ChoiceList": "СписокВыбора",
"IncompleteChoiceMode": "РежимВыбораНезаполненного",
"TextEdit": "РедактированиеТекста",
"TextEditUpdate": "ОбновлениеТекстаРедактирования",
"MultiLine": "МногострочныйРежим",
"AutoLineBreak": "АвтоПереносСтрок",
"AutoMarkIncomplete": "АвтоОтметкаНезаполненного",
"AutoChoiceIncomplete": "АвтоВыборНезаполненного",
"InputHint": "ПодсказкаВвода",
"ChoiceHistoryOnInput": "ИсторияВыбораПриВводе",
"Picture": "Картинка",
"HeaderPicture": "КартинкаШапки",
"FooterPicture": "КартинкаПодвала",
"BackColor": "ЦветФона",
"TextColor": "ЦветТекста",
"BorderColor": "ЦветРамки",
"Font": "Шрифт",
"Shape": "Фигура",
"ShapeRepresentation": "ОтображениеФигуры",
"PictureLocation": "ПоложениеКартинки",
"HeaderBackColor": "ЦветФонаЗаголовка",
"Width": "Ширина",
"Height": "Высота",
"AutoMaxWidth": "АвтоМаксимальнаяШирина",
"AutoMaxHeight": "АвтоМаксимальнаяВысота",
"MaxWidth": "МаксимальнаяШирина",
"MaxHeight": "МаксимальнаяВысота",
"TitleHeight": "ВысотаЗаголовка",
"HorizontalAlign": "ГоризонтальноеПоложениеВГруппе",
"VerticalAlign": "ВертикальноеПоложениеВГруппе",
"HorizontalStretch": "РастягиватьПоГоризонтали",
"VerticalStretch": "РастягиватьПоВертикали",
"LocationInCommandBar": "ПоложениеВКоманднойПанели",
"UniqueCommands": "УникальностьКоманд",
"DefaultButton": "КнопкаПоУмолчанию",
"Check": "Пометка",
"MainAttribute": "ОсновнойРеквизит",
"Action": "Действие",
"AdditionSource": "ИсточникДополнения",
"Columns": "Колонки",
"Save": "СохраняемыеДанные",
"ContextMenu": "КонтекстноеМеню",
"ExtendedTooltip": "РасширеннаяПодсказка",
"Events": "События",
"Handler": "Обработчик",
"ChildItems": "ПодчиненныеЭлементы",
}
FORM_XML_PROPERTY_GROUPS = {
"Основные": {
"name",
"id",
"Title",
"Type",
"DataPath",
"CommandName",
"WindowOpeningMode",
"AutoSaveDataInSettings",
"CommandBarLocation",
"ShowCommandBar",
"AutoCommandBar",
"SearchStringAddition",
"ViewStatusAddition",
"SearchControlAddition",
"TitleLocation",
"Visible",
"UserVisible",
"Enabled",
"ReadOnly",
"SkipOnInput",
"DefaultItem",
"Importance",
"ServerUnavailabilityBehavior",
"PasswordMode",
"Hyperlink",
"Representation",
"ButtonGroup",
"Group",
"Behavior",
"ShowTitle",
"Autofill",
},
"Использование": {
"EditWarningRepresentation",
"EditWarning",
"EditMode",
"AutoEditMode",
"AutoCellHeight",
"FixInTable",
"Shortcut",
"CommandSet",
"UseCopy",
"ChangeRowSet",
"RowSelectionMode",
"AutoInsertNewRow",
"EnableStartDrag",
"EnableDrag",
"RowFilter",
"FileDragMode",
"ShowInHeader",
"ShowInFooter",
"ToolTip",
"ToolTipRepresentation",
"DropListButton",
"ChoiceButton",
"ClearButton",
"SpinButton",
"OpenButton",
"CreateButton",
"QuickChoice",
"ChooseType",
"ChoiceList",
"IncompleteChoiceMode",
"TextEdit",
"TextEditUpdate",
"MultiLine",
"AutoLineBreak",
"AutoMarkIncomplete",
"AutoChoiceIncomplete",
"InputHint",
"ChoiceHistoryOnInput",
"MainAttribute",
"Action",
"Save",
},
"Оформление": {
"Picture",
"HeaderPicture",
"FooterPicture",
"BackColor",
"TextColor",
"BorderColor",
"Font",
"Shape",
"ShapeRepresentation",
"PictureLocation",
"HeaderBackColor",
"Footer",
"HorizontalLinesBWA",
"VerticalLinesBWA",
"UseAlternationRowColorBWA",
},
"Расположение": {
"Width",
"Height",
"HeightInTableRows",
"SearchStringLocation",
"ViewStatusLocation",
"SearchControlLocation",
"AutoMaxWidth",
"AutoMaxHeight",
"MaxWidth",
"MaxHeight",
"TitleHeight",
"HorizontalAlign",
"VerticalAlign",
"HorizontalStretch",
"VerticalStretch",
"LocationInCommandBar",
"UniqueCommands",
"DefaultButton",
"Check",
"AdditionSource",
"Columns",
},
}
FORM_XML_CONTAINER_TAGS = {"ChildItems", "CommandSet", "Events"}
def local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1]
def property_group(tag: str) -> str:
for group, names in FORM_XML_PROPERTY_GROUPS.items():
if tag in names:
return group
return "Прочее"
def xml_scalar(element: ET.Element) -> Any:
text = (element.text or "").strip()
if text:
if text == "true":
return True
if text == "false":
return False
return text
if "name" in element.attrib:
return element.attrib.get("name")
if "id" in element.attrib:
return element.attrib.get("id")
return None
def add_property(groups: dict[str, list[dict[str, Any]]], xml_name: str, value: Any, *, source: str = "form_xml") -> None:
if value is None:
return
group = property_group(xml_name)
groups.setdefault(group, []).append(
{
"name": FORM_XML_PROPERTY_NAMES.get(xml_name, xml_name),
"xml_name": xml_name,
"value": value,
"source": source,
"status": "ok" if value not in {"", None} else "empty",
}
)
def element_semantic(element: ET.Element) -> dict[str, Any]:
tag = local_name(element.tag)
groups: dict[str, list[dict[str, Any]]] = {}
add_property(groups, "name", element.attrib.get("name"), source="form_xml_attribute")
add_property(groups, "id", element.attrib.get("id"), source="form_xml_attribute")
add_property(groups, "Type", FORM_XML_KIND_RU.get(tag, tag), source="form_xml_tag")
if tag == "Event" and (element.text or "").strip():
add_property(groups, "Handler", (element.text or "").strip(), source="form_xml_text")
for child in list(element):
child_tag = local_name(child.tag)
if child_tag in FORM_XML_CONTAINER_TAGS:
continue
add_property(groups, child_tag, xml_scalar(child))
mapped = sum(len(items) for items in groups.values())
return {
"groups": groups,
"coverage": {
"mapped": mapped,
"unmapped": 0,
"total": mapped,
"status": "ok",
},
}
def walk_form_elements(root: ET.Element, *, limit: int = 5000) -> tuple[list[dict[str, Any]], int, bool]:
records: list[dict[str, Any]] = []
total = 0
def walk(
element: ET.Element,
path: list[int],
additional_columns_table: str | None = None,
owner_name: str | None = None,
) -> None:
nonlocal total
tag = local_name(element.tag)
if tag == "AdditionalColumns" and element.attrib.get("table"):
additional_columns_table = element.attrib.get("table")
if "name" in element.attrib:
total += 1
if len(records) < limit:
records.append(
{
"name": element.attrib.get("name"),
"id": element.attrib.get("id"),
"kind": tag,
"kind_ru": FORM_XML_KIND_RU.get(tag, tag),
"xml_path": ".".join(str(part) for part in path),
"semantic": element_semantic(element),
**({"additional_columns_table": additional_columns_table} if additional_columns_table else {}),
**({"owner": owner_name} if tag == "Event" and owner_name else {}),
}
)
child_owner = element.attrib.get("name") if "name" in element.attrib and tag != "Event" else owner_name
for index, child in enumerate(list(element)):
walk(child, [*path, index], additional_columns_table, child_owner)
walk(root, [])
return records, total, total > len(records)
def decode_form_xml(xml: str | bytes | Path, *, max_items: int = 5000) -> dict[str, Any]:
if isinstance(xml, Path):
root = ET.parse(xml).getroot()
source = {"kind": "xml_file", "path": str(xml)}
else:
root = ET.fromstring(xml)
source = {"kind": "xml_text"}
items, total, truncated = walk_form_elements(root, limit=max_items)
return {
"schema": "onec_form_xml_profile.v1",
"status": "ok",
"source": source,
"form": {
"version": root.attrib.get("version"),
"kind": local_name(root.tag),
"kind_ru": FORM_XML_KIND_RU.get(local_name(root.tag), local_name(root.tag)),
"semantic": element_semantic(root),
},
"items": items,
"counts": {
"items": len(items),
"items_total": total,
"items_truncated": truncated,
},
}