from __future__ import annotations from html import escape import json 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"""
{_topbar(project_id, project_nav)}

Права доступа не загружены

{escape(error or "NormalizedProject не найден")}

Открыть setup
""" 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"""
{_topbar(project_id, project_nav)}

1C access model

{escape(_profile_name(selected) if selected is not None else "Права доступа")}

Редактор
{render_html5_access_profile(project_id=project_id, profile=selected)}
{plan_html}
{dry_run_html}
{render_html5_access_profile_builder(project_id=project_id)}
""" return _page(f"SFERA Access - {project_id}", content) def render_html5_access_profile_builder(*, project_id: str) -> str: return f"""
Новый профиль доступа

Профиль будет построен сервером по ролям, правам и объектам 1С.

""" def render_html5_access_profile_preview(*, draft: object) -> str: roles = list(getattr(draft, "roles", []) or []) missing = list(getattr(draft, "missing_objects", []) or []) warnings = list(getattr(draft, "warnings", []) or []) proposed = dict(getattr(draft, "proposed_profile", {}) or {}) return f"""
предпросмотр {escape(str(proposed.get("qualified_name") or getattr(draft, "name", "")))}
{''.join(_role_card(role) for role in roles) or '

Роли не найдены

'}
{_notice_list("Недостающие объекты", missing)} {_notice_list("Предупреждения", warnings)}
""" def render_html5_access_profile_apply_result(*, project_id: str, response: object, plan: object | None = None) -> str: profile = getattr(response, "profile", {}) or {} profile_name = str(profile.get("name") or profile.get("qualified_name") or "") message = str(getattr(response, "message", "")) return f"""
сохранено {escape(profile_name)} Открыть

{escape(message)}

{render_html5_access_publish_plan(project_id=project_id, profile=_DictProfile(profile), plan=plan)}
""" def render_html5_access_user_detail(*, project_id: str, user_payload: dict | None) -> str: if user_payload is None: return """

Выберите пользователя, чтобы увидеть группы и эффективные роли.

""" user = dict(user_payload.get("user") or {}) roles = list(user_payload.get("effective_roles") or []) groups = list(user.get("groups") or []) name = str(user.get("name") or "") full_name = str(user.get("full_name") or "") return f"""
пользователь {escape(name)}

{escape(full_name or "ФИО не загружено")}

{_metric("Группы", len(groups))} {_metric("Эффективные роли", len(roles))}
{_notice_list("Группы пользователя", groups)}
{''.join(_role_card(_DictRole(item)) for item in roles) or '

Эффективные роли не найдены

'}
""" def render_html5_access_profile(*, project_id: str, profile: object | None) -> str: if profile is None: return """
Выберите профиль доступа План публикации и dry-run будут построены сервером по данным нормализованного объекта 1С.
""" 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"""
{escape(profile_name)} {escape(str(getattr(profile, "qualified_name", "") or ""))}
{_metric("Роли", len(roles))} {_metric("Объекты", len(target_objects))} {_metric("Права", len(permissions))}
{escape(str(getattr(profile, "source", "") or "workspace"))} {escape(str(attrs.get("status") or "loaded"))}
{''.join(_role_card(role) for role in roles) or '

Роли не назначены

'}
""" def render_html5_access_publish_plan(*, project_id: str, profile: object | None, plan: object | None) -> str: if profile is None: return '
План публикации

Нет выбранного профиля

' profile_name = _profile_name(profile) if plan is None: return f"""
План публикации
""" operations = list(getattr(plan, "operations", []) or []) warnings = list(getattr(plan, "warnings", []) or []) ready = bool(getattr(plan, "ready_for_extension", False)) warning_html = "".join(f"
  • {escape(str(item))}
  • " for item in warnings) dry_run_button = ( f"""
    """ if ready else '

    План не готов к отправке в расширение

    ' ) return f"""
    План публикации
    {'готов' if ready else 'требует проверки'} {len(operations)} операций {dry_run_button}
    {''.join(_operation_card(item) for item in operations) or '

    Операций нет

    '}
    """ def render_html5_access_publish_result(*, project_id: str, result: object | None) -> str: if result is None: return '
    Ответ расширения

    Dry-run еще не выполнялся

    ' 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"""
    Ответ расширения
    {escape(status)} {'расширение ответило' if ready else 'требуется настройка публикации'}
    {''.join(_check_card(item) for item in checks)}
    {escape(_short_json(payload))}
    """ 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""" {escape(name)} {len(roles)} ролей """ 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'
    {escape(name)}{escape(source)}
    ' 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'
    {escape(name)}{escape(profile)} · {len(users)} пользователей
    ' def _user_card(project_id: str, user: object) -> str: name = str(getattr(user, "name", "")) full_name = str(getattr(user, "full_name", "") or "") groups = list(getattr(user, "groups", []) or []) return f"""
    {escape(name)} {escape(full_name)} · {len(groups)} групп
    """ 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'
    {escape(action)}{escape(target)} {escape(detail)}
    ' def _check_card(check: object) -> str: code = str(getattr(check, "code", "")) status = str(getattr(check, "status", "")) message = str(getattr(check, "message", "")) return f'
    {escape(code)} · {escape(status)}{escape(message)}
    ' def _notice_list(title: str, values: list[object]) -> str: if not values: return "" return f"""
    {escape(title)}
    """ 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 "{}" return json.dumps({key: value for key, value in list(payload.items())[:12]}, ensure_ascii=False, indent=2, default=str) class _DictProfile: def __init__(self, payload: dict): self.name = str(payload.get("name") or "") self.qualified_name = str(payload.get("qualified_name") or self.name) self.roles = payload.get("roles") or [] self.attributes = payload.get("attributes") or {} self.source = payload.get("source") or "workspace" class _DictRole: def __init__(self, payload: dict): self.role = str(payload.get("role") or payload.get("name") or "") self.role_qualified_name = str(payload.get("role_qualified_name") or payload.get("qualified_name") or self.role) self.source = str(payload.get("source") or "")