Add HTML5 access profile workspace
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-21 19:29:35 +03:00
parent 29bbe1dca6
commit 2f7db03001
6 changed files with 420 additions and 0 deletions
@@ -45,6 +45,7 @@ def _topbar(project_id: str, project_nav: str) -> str:
<nav class="project-nav">{project_nav}</nav>
<a class="button" href="/docs">API</a>
<a class="button" href="/html5/operations">Операции</a>
<a class="button" href="/html5/projects/{quote(project_id)}/access">Права</a>
<a class="button" href="/html5/projects/{quote(project_id)}/setup">HTML5 Setup</a>
<a class="button" href="/editor?project={quote(project_id)}">Legacy Next</a>
</header>"""
@@ -0,0 +1,263 @@
from __future__ import annotations
from html import escape
from typing import Iterable
from urllib.parse import quote
from api_server.html5 import _metric, _page, _project_link, _topbar
def render_html5_access_page(
*,
project_id: str,
projects: Iterable[object],
normalized: object | None,
selected_profile: str | None = None,
plan: object | None = None,
dry_run: object | None = None,
error: str | None = None,
) -> str:
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
if error or normalized is None:
content = f"""
<main class="workspace" data-html5-page="access" data-project-id="{escape(project_id)}">
{_topbar(project_id, project_nav)}
<section class="empty-state" data-html5-error>
<h1>Права доступа не загружены</h1>
<p>{escape(error or "NormalizedProject не найден")}</p>
<a class="button" href="/html5/projects/{quote(project_id)}/setup">Открыть setup</a>
</section>
</main>
"""
return _page(f"SFERA Access - {project_id}", content)
access = getattr(normalized, "access", None)
profiles = list(getattr(access, "profiles", []) or [])
groups = list(getattr(access, "groups", []) or [])
users = list(getattr(access, "users", []) or [])
selected = _selected_profile(profiles, selected_profile)
plan_html = render_html5_access_publish_plan(project_id=project_id, profile=selected, plan=plan)
dry_run_html = render_html5_access_publish_result(project_id=project_id, result=dry_run)
content = f"""
<main class="workspace access-workspace" data-html5-page="access" data-project-id="{escape(project_id)}">
{_topbar(project_id, project_nav)}
<section class="access-layout">
<aside class="panel access-nav" data-html5-access-nav>
<div class="panel-title">Профили доступа</div>
<dl class="metrics">
{_metric("Профили", len(profiles))}
{_metric("Группы", len(groups))}
{_metric("Пользователи", len(users))}
{_metric("Назначения", _assignment_count(profiles, groups, users))}
</dl>
<nav>{''.join(_profile_link(project_id, item, selected) for item in profiles) or '<p class="muted padded">Профили не найдены</p>'}</nav>
</aside>
<section class="editor access-main" data-html5-access-main>
<div class="editor-head">
<div>
<p class="eyebrow">1C access model</p>
<h1>{escape(_profile_name(selected) if selected is not None else "Права доступа")}</h1>
</div>
<a class="button" href="/html5/projects/{quote(project_id)}/editor">Редактор</a>
</div>
{render_html5_access_profile(project_id=project_id, profile=selected)}
<div data-html5-access-plan>{plan_html}</div>
<div data-html5-access-result>{dry_run_html}</div>
</section>
<aside class="panel access-side" data-html5-access-side>
<div class="panel-title">Группы доступа</div>
<div class="access-list">{''.join(_group_card(item) for item in groups[:80]) or '<p class="muted padded">Группы не найдены</p>'}</div>
<div class="panel-title">Пользователи</div>
<div class="access-list">{''.join(_user_card(item) for item in users[:80]) or '<p class="muted padded">Пользователи не найдены</p>'}</div>
</aside>
</section>
</main>
"""
return _page(f"SFERA Access - {project_id}", content)
def render_html5_access_profile(*, project_id: str, profile: object | None) -> str:
if profile is None:
return """
<section class="access-empty">
<strong>Выберите профиль доступа</strong>
<span>План публикации и dry-run будут построены сервером по данным нормализованного объекта 1С.</span>
</section>
"""
roles = list(getattr(profile, "roles", []) or [])
attrs = dict(getattr(profile, "attributes", {}) or {})
target_objects = list(attrs.get("target_objects") or [])
permissions = list(attrs.get("permissions") or [])
profile_name = _profile_name(profile)
return f"""
<section class="access-profile" data-html5-access-profile="{escape(profile_name)}">
<div class="source-head">
<div>
<strong>{escape(profile_name)}</strong>
<small>{escape(str(getattr(profile, "qualified_name", "") or ""))}</small>
</div>
<dl>
{_metric("Роли", len(roles))}
{_metric("Объекты", len(target_objects))}
{_metric("Права", len(permissions))}
</dl>
</div>
<div class="access-summary">
<span>{escape(str(getattr(profile, "source", "") or "workspace"))}</span>
<span>{escape(str(attrs.get("status") or "loaded"))}</span>
</div>
<div class="access-role-grid">
{''.join(_role_card(role) for role in roles) or '<p class="muted padded">Роли не назначены</p>'}
</div>
</section>
"""
def render_html5_access_publish_plan(*, project_id: str, profile: object | None, plan: object | None) -> str:
if profile is None:
return '<section class="access-plan"><div class="panel-title">План публикации</div><p class="muted padded">Нет выбранного профиля</p></section>'
profile_name = _profile_name(profile)
if plan is None:
return f"""
<section class="access-plan">
<div class="panel-title">План публикации</div>
<div class="access-actions">
<button
hx-get="/html5/projects/{quote(project_id)}/access/profiles/{quote(profile_name, safe='')}/plan"
hx-target="[data-html5-access-plan]"
hx-swap="innerHTML"
>Построить план</button>
</div>
</section>
"""
operations = list(getattr(plan, "operations", []) or [])
warnings = list(getattr(plan, "warnings", []) or [])
ready = bool(getattr(plan, "ready_for_extension", False))
warning_html = "".join(f"<li>{escape(str(item))}</li>" for item in warnings)
dry_run_button = (
f"""
<form
hx-post="/html5/projects/{quote(project_id)}/access/profiles/{quote(profile_name, safe='')}/publish-dry-run"
hx-target="[data-html5-access-result]"
hx-swap="innerHTML"
>
<button type="submit" class="primary">Dry-run в 1С</button>
</form>
"""
if ready
else '<p class="muted padded">План не готов к отправке в расширение</p>'
)
return f"""
<section class="access-plan">
<div class="panel-title">План публикации</div>
<div class="access-plan-head">
<span class="status-pill">{'готов' if ready else 'требует проверки'}</span>
<strong>{len(operations)} операций</strong>
{dry_run_button}
</div>
<ul class="access-warnings">{warning_html}</ul>
<div class="access-operations">{''.join(_operation_card(item) for item in operations) or '<p class="muted padded">Операций нет</p>'}</div>
</section>
"""
def render_html5_access_publish_result(*, project_id: str, result: object | None) -> str:
if result is None:
return '<section class="access-result"><div class="panel-title">Ответ расширения</div><p class="muted padded">Dry-run еще не выполнялся</p></section>'
checks = list(getattr(result, "checks", []) or [])
payload = dict(getattr(result, "result", {}) or {})
status = str(getattr(result, "status", ""))
ready = bool(getattr(result, "ready", False))
return f"""
<section class="access-result" data-html5-access-result-status="{escape(status)}">
<div class="panel-title">Ответ расширения</div>
<div class="access-plan-head">
<span class="status-pill">{escape(status)}</span>
<strong>{'расширение ответило' if ready else 'требуется настройка публикации'}</strong>
</div>
<div class="access-operations">{''.join(_check_card(item) for item in checks)}</div>
<pre class="access-json">{escape(_short_json(payload))}</pre>
</section>
"""
def _selected_profile(profiles: list[object], selected_profile: str | None) -> object | None:
if not profiles:
return None
if not selected_profile:
return profiles[0]
wanted = selected_profile.casefold()
return next(
(
item
for item in profiles
if _profile_name(item).casefold() == wanted
or str(getattr(item, "qualified_name", "") or "").casefold() == wanted
),
profiles[0],
)
def _profile_link(project_id: str, profile: object, selected: object | None) -> str:
name = _profile_name(profile)
active = selected is profile
roles = list(getattr(profile, "roles", []) or [])
return f"""
<a class="tree-item" data-html5-access-profile-selected="{str(active).lower()}" href="/html5/projects/{quote(project_id)}/access?profile={quote(name)}">
<span>{escape(name)}</span>
<small>{len(roles)} ролей</small>
</a>
"""
def _profile_name(profile: object) -> str:
return str(getattr(profile, "name", None) or getattr(profile, "qualified_name", None) or "Профиль")
def _role_card(role: object) -> str:
name = str(getattr(role, "role_qualified_name", None) or getattr(role, "role", None) or role)
source = str(getattr(role, "source", "") or "")
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(source)}</small></article>'
def _group_card(group: object) -> str:
name = str(getattr(group, "name", ""))
profile = str(getattr(group, "profile_qualified_name", None) or getattr(group, "profile", None) or "без профиля")
users = list(getattr(group, "users", []) or [])
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(profile)} · {len(users)} пользователей</small></article>'
def _user_card(user: object) -> str:
name = str(getattr(user, "name", ""))
full_name = str(getattr(user, "full_name", "") or "")
groups = list(getattr(user, "groups", []) or [])
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(full_name)} · {len(groups)} групп</small></article>'
def _operation_card(operation: dict) -> str:
action = str(operation.get("action", "operation"))
target = str(operation.get("target", ""))
detail = str(operation.get("role") or operation.get("profile") or operation.get("name") or "")
return f'<article class="access-card"><strong>{escape(action)}</strong><small>{escape(target)} {escape(detail)}</small></article>'
def _check_card(check: object) -> str:
code = str(getattr(check, "code", ""))
status = str(getattr(check, "status", ""))
message = str(getattr(check, "message", ""))
return f'<article class="access-card"><strong>{escape(code)} · {escape(status)}</strong><small>{escape(message)}</small></article>'
def _assignment_count(profiles: list[object], groups: list[object], users: list[object]) -> int:
return (
sum(len(getattr(item, "roles", []) or []) for item in profiles)
+ sum(len(getattr(item, "roles", []) or []) + len(getattr(item, "users", []) or []) for item in groups)
+ sum(len(getattr(item, "roles", []) or []) + len(getattr(item, "groups", []) or []) for item in users)
)
def _short_json(payload: dict) -> str:
if not payload:
return "{}"
items = [f"{key}: {value}" for key, value in list(payload.items())[:12]]
return "\n".join(items)
@@ -0,0 +1,84 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable, Iterable
from typing import Any
from fastapi import HTTPException
from api_server.html5_access import (
render_html5_access_page,
render_html5_access_publish_plan,
render_html5_access_publish_result,
)
def html5_access_page(
*,
project_id: str,
profile: str | None,
project_summaries: Callable[[], Iterable[object]],
normalized_project: Callable[[str], object],
access_profile_by_name: Callable[[object, str], object | None],
access_publish_plan: Callable[[object, object], object],
) -> str:
try:
normalized = normalized_project(project_id)
selected = _selected_profile(normalized, profile, access_profile_by_name)
plan = access_publish_plan(normalized, selected) if selected is not None else None
return render_html5_access_page(
project_id=project_id,
projects=project_summaries(),
normalized=normalized,
selected_profile=profile,
plan=plan,
)
except HTTPException as error:
return render_html5_access_page(
project_id=project_id,
projects=project_summaries(),
normalized=None,
error=str(error.detail),
)
def html5_access_publish_plan(
*,
project_id: str,
profile_name: str,
normalized_project: Callable[[str], object],
access_profile_by_name: Callable[[object, str], object | None],
access_publish_plan: Callable[[object, object], object],
) -> str:
normalized = normalized_project(project_id)
profile = access_profile_by_name(normalized, profile_name)
if profile is None:
raise HTTPException(status_code=404, detail="Access profile not found")
return render_html5_access_publish_plan(
project_id=project_id,
profile=profile,
plan=access_publish_plan(normalized, profile),
)
async def html5_access_publish_dry_run(
*,
project_id: str,
profile_name: str,
publish_dry_run: Callable[[str, str], Awaitable[Any]],
) -> str:
result = await publish_dry_run(project_id, profile_name)
return render_html5_access_publish_result(project_id=project_id, result=result)
def _selected_profile(
normalized: object,
profile_name: str | None,
access_profile_by_name: Callable[[object, str], object | None],
) -> object | None:
access = getattr(normalized, "access", None)
profiles = list(getattr(access, "profiles", []) or [])
if not profiles:
return None
if not profile_name:
return profiles[0]
return access_profile_by_name(normalized, profile_name) or profiles[0]
@@ -55,6 +55,11 @@ from api_server.agent_models import (
from api_server.html5_forms import (
html5_form_data as _html5_form_data,
)
from api_server.html5_access_controller import (
html5_access_page as _html5_access_page,
html5_access_publish_dry_run as _html5_access_publish_dry_run,
html5_access_publish_plan as _html5_access_publish_plan,
)
from api_server.html5_editor_controller import (
html5_editor_page as _html5_editor_page,
html5_form_editor_page as _html5_form_editor_page,
@@ -1532,6 +1537,44 @@ async def html5_project_form_editor(project_id: str, form: str | None = None) ->
)
@app.get("/html5/projects/{project_id}/access")
async def html5_project_access(project_id: str, profile: str | None = None) -> Response:
return _html5_response(
_html5_access_page(
project_id=project_id,
profile=profile,
project_summaries=_project_summaries,
normalized_project=_normalized_project_or_404,
access_profile_by_name=_access_profile_by_name,
access_publish_plan=_build_access_profile_publish_plan,
)
)
@app.get("/html5/projects/{project_id}/access/profiles/{profile_name}/plan")
async def html5_project_access_profile_plan(project_id: str, profile_name: str) -> Response:
return _html5_response(
_html5_access_publish_plan(
project_id=project_id,
profile_name=profile_name,
normalized_project=_normalized_project_or_404,
access_profile_by_name=_access_profile_by_name,
access_publish_plan=_build_access_profile_publish_plan,
)
)
@app.post("/html5/projects/{project_id}/access/profiles/{profile_name}/publish-dry-run")
async def html5_project_access_profile_publish_dry_run(project_id: str, profile_name: str) -> Response:
return _html5_response(
await _html5_access_publish_dry_run(
project_id=project_id,
profile_name=profile_name,
publish_dry_run=dry_run_project_access_profile_publish,
)
)
@app.post("/html5/projects/{project_id}/forms/editor/preview")
async def html5_project_form_editor_preview(project_id: str, request: Request) -> Response:
form = await _html5_form_data(request)
@@ -2927,6 +2970,13 @@ async def get_normalized_project(project_id: str) -> NormalizedProject:
return normalized
def _normalized_project_or_404(project_id: str) -> NormalizedProject:
normalized = _load_normalized_project(project_id)
if normalized is None:
raise HTTPException(status_code=404, detail="NormalizedProject not found")
return normalized
@app.get("/projects/{project_id}/normalized/summary", response_model=NormalizedProjectSummary)
async def get_normalized_project_summary(project_id: str) -> NormalizedProjectSummary:
normalized = _load_normalized_project(project_id)
@@ -10,6 +10,8 @@
.form-editor-layout{grid-template-columns:280px minmax(0,1.2fr) minmax(320px,.8fr)}.form-editor-actions{display:flex;gap:8px;flex-wrap:wrap}.tree-item[data-html5-form-selected="true"]{background:#f8fbff;border-left-color:var(--brand)}.form-designer{height:calc(100% - 72px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden;background:#f7f9fc}.form-designer-head,.form-designer-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line);background:#fff}.form-designer-head strong,.form-designer-head small{display:block}.form-designer-head small{color:var(--muted)}.form-designer-head span{border:1px solid var(--line);background:#eef6f1;color:var(--ok);padding:5px 8px;font-size:12px;font-weight:900}.form-designer-foot{border-top:1px solid var(--line);border-bottom:0}.form-canvas{min-height:0;overflow:auto;padding:18px}.form-window{max-width:720px;min-height:420px;margin:0 auto;border:1px solid var(--line);background:#fff;box-shadow:0 8px 24px rgba(15,23,42,.08)}.form-window-title{height:38px;display:flex;align-items:center;border-bottom:1px solid var(--line);padding:0 12px;font-weight:900;background:#fbfcfe}.form-command-bar{display:flex;gap:6px;flex-wrap:wrap;padding:10px 12px;border-bottom:1px solid var(--line)}.form-command-bar button{height:28px;font-size:12px}.form-fields{display:grid;gap:10px;padding:14px}.form-field{display:grid;grid-template-columns:160px minmax(0,1fr);gap:8px;align-items:center}.form-field span{font-size:12px;font-weight:800;color:var(--muted)}.form-field input{height:30px;border:1px solid var(--line);padding:0 8px;background:#fbfaf7}
.form-designer-body{min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 340px;overflow:hidden}.form-property-panel{min-height:0;overflow:auto;border-left:1px solid var(--line);background:#fff}.property-row{display:grid;grid-template-columns:1fr 132px;gap:8px;padding:12px;border-bottom:1px solid var(--line)}.property-row label,.form-editor-row label{display:grid;gap:4px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.property-row input,.property-row select,.form-editor-row input,.form-editor-row select,.form-add-row input,.form-add-row select{height:30px;min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.2 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.form-window{border-color:#b9c1cd;background:#fdfdfd;box-shadow:0 12px 28px rgba(33,43,54,.12)}.form-window-title{justify-content:space-between;height:34px;background:linear-gradient(#f8f9fb,#e9edf3);border-bottom-color:#cbd3df;color:#1f2937}.form-window-title small{font-size:11px;color:#687385;font-weight:800}.form-command-bar{min-height:42px;background:#f4f6f9;border-bottom-color:#ccd4df}.form-command-bar button{height:27px;border-color:#aeb8c6;background:linear-gradient(#fff,#edf1f6);box-shadow:inset 0 1px 0 #fff;color:#1f2937}.form-command-bar button:hover{background:#fff}.form-fields{grid-template-columns:repeat(12,minmax(0,1fr));align-content:start;gap:8px 12px;padding:14px 16px 22px;background:#fbfbfc}.form-field{grid-column:1/-1;grid-template-columns:150px minmax(0,1fr);min-height:32px;padding:4px 6px;border:1px solid transparent}.form-field:hover{border-color:#b9c1cd;background:#fff}.form-field[data-html5-form-width="half"]{grid-column:span 6}.form-field[data-html5-form-width="third"]{grid-column:span 4}.form-field>label{font-size:12px;font-weight:800;color:#4b5563;align-self:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.form-input-control,.form-text-control{width:100%;border:1px solid #aeb8c6;background:#fff;padding:0 7px;color:#17202a;box-shadow:inset 0 1px 2px rgba(15,23,42,.06)}.form-input-control{height:28px}.form-text-control{min-height:64px;resize:none}.form-check-control{display:flex;align-items:center;gap:8px;color:#374151}.form-check-control input{width:16px;height:16px}.form-table-control{border:1px solid #aeb8c6;background:#fff;min-height:108px;overflow:hidden}.form-table-control div{display:grid;grid-template-columns:2fr 1fr 1fr}.form-table-control span{min-height:27px;border-right:1px solid #d7dde6;border-bottom:1px solid #d7dde6;padding:5px 7px;color:#374151;font-size:12px}.form-table-control div:first-child span{background:#eef2f7;font-weight:900}.form-group-control{min-height:44px;border:1px dashed #aeb8c6;background:#f6f8fb;padding:10px;color:#687385;font-weight:800}.form-editor-row{display:grid;gap:7px;padding:10px 12px;border-bottom:1px solid var(--line)}label.form-editor-row{grid-template-columns:88px minmax(0,1fr);align-items:center;text-transform:none}.form-editor-row small{color:var(--muted);font-size:11px}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr 118px 1fr 104px}.form-add-row{display:grid;grid-template-columns:minmax(0,1fr) 118px;gap:8px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.button.primary{background:var(--brand);border-color:var(--brand);color:#fff;margin:12px}.form-designer-foot small{color:var(--muted);font-weight:800}.form-window[data-html5-form-layout="compact"] .form-fields{gap:4px 8px}.form-window[data-html5-form-layout="compact"] .form-field{min-height:28px}.form-window[data-html5-form-layout="columns"] .form-field{grid-column:span 6}.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="table"],.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="group"]{grid-column:1/-1}
.form-empty-structure{grid-column:1/-1;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.form-empty-structure strong,.form-empty-structure span{display:block}.form-empty-structure strong{color:#1f2937}
.access-workspace{background:#f4f7fb}.access-layout{display:grid;grid-template-columns:300px minmax(0,1fr)340px;height:calc(100vh - 88px)}.access-main{border-top:0;border-bottom:0;overflow:auto}.access-nav,.access-side{overflow:auto}.tree-item[data-html5-access-profile-selected="true"]{background:#f8fbff;border-left:3px solid var(--brand);padding-left:9px}.access-empty{margin:16px;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.access-empty strong,.access-empty span{display:block}.access-empty strong{color:#1f2937}.access-profile,.access-plan,.access-result{border-bottom:1px solid var(--line);background:#fff}.access-summary{display:flex;gap:8px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line);background:#f8fbff;color:#687385;font-size:12px;font-weight:800}.access-role-grid,.access-operations,.access-list{display:grid}.access-role-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.access-card{display:grid;gap:3px;min-width:0;padding:10px 12px;border-bottom:1px solid var(--line)}.access-role-grid .access-card:nth-child(odd){border-right:1px solid var(--line)}.access-card strong,.access-card small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.access-card small{color:var(--muted)}.access-plan-head{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-plan-head form{margin:0}.access-actions{padding:12px}.access-warnings{margin:0;padding:0;list-style:none}.access-warnings li{padding:8px 12px;border-bottom:1px solid var(--line);color:var(--warn);font-weight:800}.access-json{margin:0;max-height:220px;overflow:auto;padding:12px;background:#fbfaf7;border-top:1px solid var(--line);font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap}.access-main .primary{background:var(--brand);border-color:var(--brand);color:#fff}
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
@media(max-width:980px){.access-layout{grid-template-columns:1fr;height:auto}.access-nav,.access-side{max-height:360px}.access-role-grid{grid-template-columns:1fr}.access-role-grid .access-card:nth-child(odd){border-right:0}}
@media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}}
@media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}}
+20
View File
@@ -1610,6 +1610,26 @@ def test_import_supports_structure_only_indexing(monkeypatch, tmp_path: Path):
assert captured_extension_call["request"].dry_run is True
assert captured_extension_call["request"].payload["profile"]["roles"] == ["Роль.Менеджер"]
access_page = client.get(f"/html5/projects/{project_id}/access", params={"profile": "НовыйПрофильHTTP"})
assert_html5_response_contract(
access_page,
'data-html5-page="access"',
"НовыйПрофильHTTP",
"План публикации",
"Dry-run в 1С",
full_page=True,
)
access_plan = client.get(
f"/html5/projects/{project_id}/access/profiles/{quote('НовыйПрофильHTTP')}/plan"
)
assert_html5_response_contract(access_plan, "CREATE_ACCESS_PROFILE", "ADD_ROLE_TO_PROFILE")
access_dry_run = client.post(
f"/html5/projects/{project_id}/access/profiles/{quote('НовыйПрофильHTTP')}/publish-dry-run"
)
assert_html5_response_contract(access_dry_run, "Ответ расширения", "READY", "access.profile.apply")
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
root = tree.json()["root"]