Add HTML5 metadata authoring form
This commit is contained in:
@@ -233,6 +233,7 @@ def render_html5_editor(
|
|||||||
{render_html5_project_report(project_id, None)}
|
{render_html5_project_report(project_id, None)}
|
||||||
{render_html5_review(project_id, None)}
|
{render_html5_review(project_id, None)}
|
||||||
{render_html5_authoring_preview(project_id, None)}
|
{render_html5_authoring_preview(project_id, None)}
|
||||||
|
{render_html5_metadata_authoring(project_id)}
|
||||||
{render_html5_authoring_changes(project_id, None)}
|
{render_html5_authoring_changes(project_id, None)}
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
@@ -508,6 +509,113 @@ def render_html5_authoring_apply_result(project_id: str, result: object | None =
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_metadata_authoring(project_id: str) -> str:
|
||||||
|
return f"""
|
||||||
|
<div class="authoring-preview" data-html5-metadata-authoring>
|
||||||
|
<div class="panel-title">Metadata draft</div>
|
||||||
|
<form
|
||||||
|
class="authoring-preview-form"
|
||||||
|
data-html5-metadata-preview-form
|
||||||
|
method="post"
|
||||||
|
action="/html5/projects/{quote(project_id)}/authoring/metadata-object-preview"
|
||||||
|
hx-post="/html5/projects/{quote(project_id)}/authoring/metadata-object-preview"
|
||||||
|
hx-target="[data-html5-metadata-preview-result]"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<input name="object_kind" value="DOCUMENT" />
|
||||||
|
<input name="name" placeholder="Имя объекта" required />
|
||||||
|
<input name="synonym" placeholder="Синоним" />
|
||||||
|
<input name="attributes" placeholder="Реквизиты: Имя:Тип, ..." />
|
||||||
|
<input name="tabular_sections" placeholder="ТЧ: Товары[Номенклатура:Строка;Количество:Число]" />
|
||||||
|
<input name="forms" placeholder="Формы через запятую" />
|
||||||
|
<input name="commands" placeholder="Команды: Имя:Обработчик" />
|
||||||
|
<input name="task_id" placeholder="task_id" />
|
||||||
|
<input name="session_id" placeholder="session_id" />
|
||||||
|
<input name="user_id" placeholder="user_id" />
|
||||||
|
<button type="submit">Metadata preview</button>
|
||||||
|
</form>
|
||||||
|
{render_html5_metadata_preview_result(project_id)}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_metadata_preview_result(
|
||||||
|
project_id: str,
|
||||||
|
preview: object | None = None,
|
||||||
|
error: str | None = None,
|
||||||
|
request_payload: dict | None = None,
|
||||||
|
) -> str:
|
||||||
|
if preview is None and error is None:
|
||||||
|
return '<div class="metadata-preview-result" data-html5-metadata-preview-result></div>'
|
||||||
|
if error:
|
||||||
|
return f"""
|
||||||
|
<div class="metadata-preview-result" data-html5-metadata-preview-result>
|
||||||
|
<div class="panel-title">Metadata preview</div>
|
||||||
|
<p class="muted padded">{escape(error)}</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
changed = bool(getattr(preview, "changed", False))
|
||||||
|
added = getattr(preview, "added_lines", 0)
|
||||||
|
target = getattr(preview, "target", None)
|
||||||
|
target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
|
||||||
|
checks = getattr(preview, "checks", []) or []
|
||||||
|
diff = getattr(preview, "semantic_diff", []) or []
|
||||||
|
version_preview = getattr(preview, "version_preview", None)
|
||||||
|
next_version_id = str(getattr(version_preview, "next_version_id", ""))
|
||||||
|
check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
|
||||||
|
diff_rows = "".join(_authoring_diff_item(line) for line in diff[:12]) or '<p class="muted padded">Diff пустой</p>'
|
||||||
|
apply_form = (
|
||||||
|
_metadata_apply_form(project_id, request_payload or {}, next_version_id)
|
||||||
|
if changed and next_version_id
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
return f"""
|
||||||
|
<div class="metadata-preview-result" data-html5-metadata-preview-result data-html5-project-id="{escape(project_id)}">
|
||||||
|
<div class="panel-title">Metadata preview · {'changed' if changed else 'unchanged'}</div>
|
||||||
|
<article class="authoring-change">
|
||||||
|
<strong>{escape(str(target_name))}</strong>
|
||||||
|
<span>+{escape(str(added))} / -0</span>
|
||||||
|
<small>{escape(next_version_id or "version preview unavailable")}</small>
|
||||||
|
</article>
|
||||||
|
<div class="check-list">{check_rows}</div>
|
||||||
|
<div class="diff-list">{diff_rows}</div>
|
||||||
|
{apply_form}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_metadata_apply_result(project_id: str, result: object | None = None, error: str | None = None) -> str:
|
||||||
|
if result is None and error is None:
|
||||||
|
return '<div class="metadata-apply-result" data-html5-metadata-apply-result></div>'
|
||||||
|
if error:
|
||||||
|
return f"""
|
||||||
|
<div class="metadata-apply-result" data-html5-metadata-apply-result>
|
||||||
|
<div class="panel-title">Metadata apply</div>
|
||||||
|
<p class="muted padded">{escape(error)}</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
status = str(getattr(result, "status", "UNKNOWN"))
|
||||||
|
change_id = str(getattr(result, "change_id", ""))
|
||||||
|
version = getattr(result, "version", None)
|
||||||
|
version_id = str(getattr(version, "version_id", ""))
|
||||||
|
return f"""
|
||||||
|
<div
|
||||||
|
class="metadata-apply-result"
|
||||||
|
data-html5-metadata-apply-result
|
||||||
|
data-html5-authoring-change="{escape(change_id)}"
|
||||||
|
data-html5-version-id="{escape(version_id)}"
|
||||||
|
>
|
||||||
|
<div class="panel-title">Metadata apply</div>
|
||||||
|
<article class="authoring-change">
|
||||||
|
<strong>{escape(status)}</strong>
|
||||||
|
<span>{escape(change_id)}</span>
|
||||||
|
<small>{escape(version_id)}</small>
|
||||||
|
</article>
|
||||||
|
<p class="muted padded">Metadata draft применен в workspace для проекта {escape(project_id)}.</p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_html5_authoring_change_detail(project_id: str, preview: object | None) -> str:
|
def render_html5_authoring_change_detail(project_id: str, preview: object | None) -> str:
|
||||||
if preview is None:
|
if preview is None:
|
||||||
return f"""
|
return f"""
|
||||||
@@ -1248,6 +1356,36 @@ def _authoring_apply_change_set_form(project_id: str, payload: dict, next_versio
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_apply_form(project_id: str, payload: dict, next_version_id: str) -> str:
|
||||||
|
return f"""
|
||||||
|
<form
|
||||||
|
class="authoring-preview-form"
|
||||||
|
data-html5-metadata-apply-form
|
||||||
|
method="post"
|
||||||
|
action="/html5/projects/{quote(project_id)}/authoring/apply-metadata-object"
|
||||||
|
hx-post="/html5/projects/{quote(project_id)}/authoring/apply-metadata-object"
|
||||||
|
hx-target="[data-html5-metadata-apply-result]"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="object_kind" value="{escape(str(payload.get("object_kind") or ""))}" />
|
||||||
|
<input type="hidden" name="name" value="{escape(str(payload.get("name") or ""))}" />
|
||||||
|
<input type="hidden" name="synonym" value="{escape(str(payload.get("synonym") or ""))}" />
|
||||||
|
<input type="hidden" name="attributes" value="{escape(str(payload.get("_raw_attributes") or ""))}" />
|
||||||
|
<input type="hidden" name="tabular_sections" value="{escape(str(payload.get("_raw_tabular_sections") or ""))}" />
|
||||||
|
<input type="hidden" name="forms" value="{escape(str(payload.get("_raw_forms") or ""))}" />
|
||||||
|
<input type="hidden" name="commands" value="{escape(str(payload.get("_raw_commands") or ""))}" />
|
||||||
|
<input type="hidden" name="task_id" value="{escape(str(payload.get("task_id") or ""))}" />
|
||||||
|
<input type="hidden" name="session_id" value="{escape(str(payload.get("session_id") or ""))}" />
|
||||||
|
<input type="hidden" name="user_id" value="{escape(str(payload.get("user_id") or ""))}" />
|
||||||
|
<input type="hidden" name="expected_next_version_id" value="{escape(next_version_id)}" />
|
||||||
|
<input name="approved_by" placeholder="approved_by" required />
|
||||||
|
<input name="approval_note" placeholder="Комментарий" />
|
||||||
|
<button type="submit">Apply metadata draft</button>
|
||||||
|
</form>
|
||||||
|
{render_html5_metadata_apply_result(project_id)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _node_source_text(node: object | None) -> str:
|
def _node_source_text(node: object | None) -> str:
|
||||||
if node is None:
|
if node is None:
|
||||||
return "// Модуль не найден в snapshot.\n// HTML5 IDE показывает серверный fallback без клиентского JS."
|
return "// Модуль не найден в snapshot.\n// HTML5 IDE показывает серверный fallback без клиентского JS."
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ from api_server.html5 import (
|
|||||||
render_html5_authoring_rollback_result,
|
render_html5_authoring_rollback_result,
|
||||||
render_html5_editor,
|
render_html5_editor,
|
||||||
render_html5_index,
|
render_html5_index,
|
||||||
|
render_html5_metadata_apply_result,
|
||||||
|
render_html5_metadata_preview_result,
|
||||||
render_html5_project_setup,
|
render_html5_project_setup,
|
||||||
render_html5_project_rows,
|
render_html5_project_rows,
|
||||||
render_html5_project_report,
|
render_html5_project_report,
|
||||||
@@ -1851,6 +1853,39 @@ async def html5_project_authoring_apply_change_set(project_id: str, request: Req
|
|||||||
return Response(html, media_type="text/html; charset=utf-8")
|
return Response(html, media_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/html5/projects/{project_id}/authoring/metadata-object-preview")
|
||||||
|
async def html5_project_authoring_metadata_object_preview(project_id: str, request: Request) -> Response:
|
||||||
|
form = await _html5_form_data(request)
|
||||||
|
raw_payload = _html5_metadata_payload(form)
|
||||||
|
payload = AuthoringMetadataObjectPreviewRequest(**_html5_metadata_request_payload(raw_payload))
|
||||||
|
try:
|
||||||
|
preview = _authoring_metadata_object_preview(project_id, payload)
|
||||||
|
html = render_html5_metadata_preview_result(project_id, preview, request_payload=raw_payload)
|
||||||
|
except (HTTPException, ValueError) as error:
|
||||||
|
detail = getattr(error, "detail", str(error))
|
||||||
|
html = render_html5_metadata_preview_result(project_id, error=str(detail))
|
||||||
|
return Response(html, media_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/html5/projects/{project_id}/authoring/apply-metadata-object")
|
||||||
|
async def html5_project_authoring_apply_metadata_object(project_id: str, request: Request) -> Response:
|
||||||
|
form = await _html5_form_data(request)
|
||||||
|
raw_payload = _html5_metadata_payload(form)
|
||||||
|
payload = AuthoringApplyMetadataObjectRequest(
|
||||||
|
**_html5_metadata_request_payload(raw_payload),
|
||||||
|
expected_next_version_id=_form_value(form, "expected_next_version_id") or "",
|
||||||
|
approved_by=_form_value(form, "approved_by") or "",
|
||||||
|
approval_note=_form_value(form, "approval_note"),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = await authoring_apply_metadata_object(project_id, payload)
|
||||||
|
html = render_html5_metadata_apply_result(project_id, result)
|
||||||
|
except (HTTPException, ValueError) as error:
|
||||||
|
detail = getattr(error, "detail", str(error))
|
||||||
|
html = render_html5_metadata_apply_result(project_id, error=str(detail))
|
||||||
|
return Response(html, media_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/html5/projects/{project_id}/setup")
|
@app.get("/html5/projects/{project_id}/setup")
|
||||||
async def html5_project_setup(project_id: str) -> Response:
|
async def html5_project_setup(project_id: str) -> Response:
|
||||||
setup = _project_setup_response(project_id)
|
setup = _project_setup_response(project_id)
|
||||||
@@ -8004,6 +8039,66 @@ def _form_value(form: dict[str, list[str]], key: str) -> str | None:
|
|||||||
return value or None
|
return value or None
|
||||||
|
|
||||||
|
|
||||||
|
def _html5_metadata_payload(form: dict[str, list[str]]) -> dict:
|
||||||
|
return {
|
||||||
|
"object_kind": _form_value(form, "object_kind") or "DOCUMENT",
|
||||||
|
"name": _form_value(form, "name") or "",
|
||||||
|
"synonym": _form_value(form, "synonym"),
|
||||||
|
"attributes": _html5_metadata_attributes(_form_value(form, "attributes") or ""),
|
||||||
|
"tabular_sections": _html5_metadata_tabular_sections(_form_value(form, "tabular_sections") or ""),
|
||||||
|
"forms": _html5_csv_values(_form_value(form, "forms") or ""),
|
||||||
|
"commands": _html5_metadata_commands(_form_value(form, "commands") or ""),
|
||||||
|
"task_id": _form_value(form, "task_id"),
|
||||||
|
"session_id": _form_value(form, "session_id"),
|
||||||
|
"user_id": _form_value(form, "user_id"),
|
||||||
|
"_raw_attributes": _form_value(form, "attributes") or "",
|
||||||
|
"_raw_tabular_sections": _form_value(form, "tabular_sections") or "",
|
||||||
|
"_raw_forms": _form_value(form, "forms") or "",
|
||||||
|
"_raw_commands": _form_value(form, "commands") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _html5_metadata_request_payload(payload: dict) -> dict:
|
||||||
|
return {key: value for key, value in payload.items() if not key.startswith("_raw_")}
|
||||||
|
|
||||||
|
|
||||||
|
def _html5_csv_values(raw: str) -> list[str]:
|
||||||
|
return [item.strip() for item in raw.replace("\n", ",").split(",") if item.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def _html5_metadata_attributes(raw: str) -> list[dict]:
|
||||||
|
attributes: list[dict] = []
|
||||||
|
for item in _html5_csv_values(raw):
|
||||||
|
name, _, type_name = item.partition(":")
|
||||||
|
if name.strip():
|
||||||
|
attributes.append({"name": name.strip(), "type": type_name.strip() or "Строка"})
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def _html5_metadata_commands(raw: str) -> list[dict]:
|
||||||
|
commands: list[dict] = []
|
||||||
|
for item in _html5_csv_values(raw):
|
||||||
|
name, _, handler = item.partition(":")
|
||||||
|
if name.strip():
|
||||||
|
commands.append({"name": name.strip(), "handler": handler.strip() or None})
|
||||||
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
def _html5_metadata_tabular_sections(raw: str) -> list[dict]:
|
||||||
|
sections: list[dict] = []
|
||||||
|
for item in _html5_csv_values(raw):
|
||||||
|
name, _, attrs = item.partition("[")
|
||||||
|
if not name.strip():
|
||||||
|
continue
|
||||||
|
attributes = []
|
||||||
|
for attr in attrs.rstrip("]").split(";"):
|
||||||
|
attr_name, _, attr_type = attr.partition(":")
|
||||||
|
if attr_name.strip():
|
||||||
|
attributes.append({"name": attr_name.strip(), "type": attr_type.strip() or "Строка"})
|
||||||
|
sections.append({"name": name.strip(), "attributes": attributes})
|
||||||
|
return sections
|
||||||
|
|
||||||
|
|
||||||
def _current_import_source(project_id: str) -> ImportSourceKind:
|
def _current_import_source(project_id: str) -> ImportSourceKind:
|
||||||
setup = _project_setup_response(project_id)
|
setup = _project_setup_response(project_id)
|
||||||
if setup.current_source is not None:
|
if setup.current_source is not None:
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ def test_html5_server_rendered_project_editor(tmp_path: Path):
|
|||||||
assert f'hx-post="/html5/projects/{project_id}/authoring/completion-preview"' in editor.text
|
assert f'hx-post="/html5/projects/{project_id}/authoring/completion-preview"' in editor.text
|
||||||
assert "data-html5-authoring-diff-form" in editor.text
|
assert "data-html5-authoring-diff-form" in editor.text
|
||||||
assert f'hx-post="/html5/projects/{project_id}/authoring/semantic-diff-preview"' in editor.text
|
assert f'hx-post="/html5/projects/{project_id}/authoring/semantic-diff-preview"' in editor.text
|
||||||
|
assert "data-html5-metadata-authoring" in editor.text
|
||||||
|
assert "data-html5-metadata-preview-form" in editor.text
|
||||||
|
assert f'hx-post="/html5/projects/{project_id}/authoring/metadata-object-preview"' in editor.text
|
||||||
assert "data-html5-authoring-changes" in editor.text
|
assert "data-html5-authoring-changes" in editor.text
|
||||||
assert f'hx-get="/html5/projects/{project_id}/authoring/changes"' in editor.text
|
assert f'hx-get="/html5/projects/{project_id}/authoring/changes"' in editor.text
|
||||||
assert 'hx-get="/html5/projects/' in editor.text
|
assert 'hx-get="/html5/projects/' in editor.text
|
||||||
@@ -2723,6 +2726,53 @@ def test_authoring_metadata_object_preview_and_apply(tmp_path: Path):
|
|||||||
assert any("ТабличнаяЧасть.Товары" in row["text"] for row in preview_payload["semantic_diff"])
|
assert any("ТабличнаяЧасть.Товары" in row["text"] for row in preview_payload["semantic_diff"])
|
||||||
assert any("Команда.Заполнить" in row["text"] for row in preview_payload["semantic_diff"])
|
assert any("Команда.Заполнить" in row["text"] for row in preview_payload["semantic_diff"])
|
||||||
|
|
||||||
|
html5_preview = client.post(
|
||||||
|
f"/html5/projects/{project_id}/authoring/metadata-object-preview",
|
||||||
|
data={
|
||||||
|
"object_kind": "DOCUMENT",
|
||||||
|
"name": "ЗаявкаНаЗакупкуHtml5",
|
||||||
|
"synonym": "Заявка на закупку HTML5",
|
||||||
|
"attributes": "Контрагент:СправочникСсылка.Контрагенты",
|
||||||
|
"tabular_sections": "Товары[Номенклатура:СправочникСсылка.Номенклатура;Количество:Число]",
|
||||||
|
"forms": "ФормаДокумента",
|
||||||
|
"commands": "Заполнить:ЗаполнитьКоманда",
|
||||||
|
"task_id": "task.metadata",
|
||||||
|
"session_id": "session.metadata",
|
||||||
|
"user_id": "dev.ivan",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert html5_preview.status_code == 200
|
||||||
|
assert "text/html" in html5_preview.headers["content-type"]
|
||||||
|
assert "data-html5-metadata-preview-result" in html5_preview.text
|
||||||
|
assert "data-html5-metadata-apply-form" in html5_preview.text
|
||||||
|
assert f'hx-post="/html5/projects/{project_id}/authoring/apply-metadata-object"' in html5_preview.text
|
||||||
|
assert "Документ.ЗаявкаНаЗакупкуHtml5" in html5_preview.text
|
||||||
|
assert "Реквизит.Контрагент" in html5_preview.text
|
||||||
|
assert "<html" not in html5_preview.text
|
||||||
|
|
||||||
|
html5_blocked_apply = client.post(
|
||||||
|
f"/html5/projects/{project_id}/authoring/apply-metadata-object",
|
||||||
|
data={
|
||||||
|
"object_kind": "DOCUMENT",
|
||||||
|
"name": "ЗаявкаНаЗакупкуHtml5",
|
||||||
|
"synonym": "Заявка на закупку HTML5",
|
||||||
|
"attributes": "Контрагент:СправочникСсылка.Контрагенты",
|
||||||
|
"tabular_sections": "Товары[Номенклатура:СправочникСсылка.Номенклатура;Количество:Число]",
|
||||||
|
"forms": "ФормаДокумента",
|
||||||
|
"commands": "Заполнить:ЗаполнитьКоманда",
|
||||||
|
"task_id": "task.metadata",
|
||||||
|
"session_id": "session.metadata",
|
||||||
|
"user_id": "dev.ivan",
|
||||||
|
"expected_next_version_id": "wrong-version",
|
||||||
|
"approved_by": "dev.ivan",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert html5_blocked_apply.status_code == 200
|
||||||
|
assert "text/html" in html5_blocked_apply.headers["content-type"]
|
||||||
|
assert "data-html5-metadata-apply-result" in html5_blocked_apply.text
|
||||||
|
assert "Expected version id does not match current metadata preview" in html5_blocked_apply.text
|
||||||
|
assert "<html" not in html5_blocked_apply.text
|
||||||
|
|
||||||
apply_response = client.post(
|
apply_response = client.post(
|
||||||
f"/projects/{project_id}/authoring/apply-metadata-object",
|
f"/projects/{project_id}/authoring/apply-metadata-object",
|
||||||
json={
|
json={
|
||||||
|
|||||||
Reference in New Issue
Block a user