Files
govoplan-dist-lists/src/govoplan_dist_lists/backend/manifest.py
T

362 lines
13 KiB
Python

from __future__ import annotations
from pathlib import Path
from govoplan_core.core.access import (
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
)
from govoplan_core.core.dataflows import CAPABILITY_DATAFLOW_DATASET_OUTPUT
from govoplan_core.core.contact_points import (
CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION,
)
from govoplan_core.core.distribution_lists import (
CAPABILITY_DISTRIBUTION_LIST_EXPAND,
CAPABILITY_DISTRIBUTION_LIST_SOURCE,
CAPABILITY_DISTRIBUTION_LIST_WRITER,
CAPABILITY_POLICY_DISTRIBUTION_CHANNELS,
CAPABILITY_RECIPIENT_CHANNEL_FACTS,
)
from govoplan_core.core.identity import (
CAPABILITY_IDENTITY_DIRECTORY,
CAPABILITY_IDENTITY_SEARCH,
)
from govoplan_core.core.idm import (
CAPABILITY_IDM_FUNCTION_ASSIGNMENTS,
CAPABILITY_IDM_RELATIONSHIPS,
)
from govoplan_core.core.module_guards import (
drop_table_retirement_provider,
persistent_table_uninstall_guard,
)
from govoplan_core.core.modules import (
DocumentationTopic,
FrontendModule,
FrontendRoute,
MigrationSpec,
ModuleContext,
ModuleInterfaceProvider,
ModuleInterfaceRequirement,
ModuleManifest,
NavItem,
PermissionDefinition,
RoleTemplate,
)
from govoplan_core.core.provider_governance import declared_module_architecture
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_dist_lists.backend.db import models as dist_list_models
MODULE_ID = "dist_lists"
MODULE_NAME = "Distribution Lists"
MODULE_VERSION = "0.1.14"
READ_SCOPE = "dist_lists:list:read"
WRITE_SCOPE = "dist_lists:list:write"
ADMIN_SCOPE = "dist_lists:list:admin"
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
module_id, resource, action = scope.split(":", 2)
return PermissionDefinition(
scope=scope,
label=label,
description=description,
category=MODULE_NAME,
level="tenant",
module_id=module_id,
resource=resource,
action=action,
)
PERMISSIONS = (
_permission(READ_SCOPE, "View distribution lists", "Read operational distribution lists and expansion previews."),
_permission(WRITE_SCOPE, "Manage distribution lists", "Create and edit operational distribution lists."),
_permission(ADMIN_SCOPE, "Administer distribution lists", "Configure distribution-list policies and provider integrations."),
)
ROLE_TEMPLATES = (
RoleTemplate(
slug="distribution_list_manager",
name="Distribution list manager",
description="Create, manage, preview, and reuse operational distribution lists.",
permissions=(READ_SCOPE, WRITE_SCOPE),
),
)
DOCUMENTATION = (
DocumentationTopic(
id=f"{MODULE_ID}.boundary",
title="Distribution list boundary",
summary="Distribution lists model operational Verteiler separately from address-book lists and workflow Umlauf execution.",
body=(
"Distribution Lists owns reusable mixed recipient definitions and expansion snapshots. "
"Address lists remain in the addresses module and represent address-domain groupings only. "
"Workflow and Tasks own Umlauf execution state, ordering, deadlines, escalation, and completion."
),
layer="available",
documentation_types=("admin", "user"),
audience=("operator", "module_admin", "product_owner"),
related_modules=("addresses", "campaigns", "mail", "postbox", "notifications", "scheduling", "poll", "workflow_engine", "tasks"),
metadata={"seed": True},
),
DocumentationTopic(
id=f"{MODULE_ID}.address-contact-resolution",
title="Address contact-point resolution",
summary="Resolve address contacts and lists into purpose-aware email and postal candidates.",
body=(
"When Addresses is enabled, Distribution Lists uses its versioned contact-point contract "
"to resolve communication purpose, requested channels, address purpose, fallback, locale, "
"quality, preference, consent, and suppression. Expansion previews explain rejected points. "
"Frozen Distribution List snapshots retain the exact rendered email or postal target, contact-"
"point ID, source revision and fingerprint, and decision provenance. If the new contract is "
"unavailable, older Addresses lookup and email-source capabilities remain a compatibility fallback."
),
layer="available",
documentation_types=("admin", "user"),
audience=("operator", "module_admin", "product_owner"),
related_modules=("addresses", "campaigns", "policy"),
metadata={"seed": True},
),
DocumentationTopic(
id=f"{MODULE_ID}.idm-group-resolution",
title="Effective IDM group audiences",
summary="Typed IDM groups expand into explainable, effective-dated identity recipients.",
body=(
"When IDM is enabled, an IDM group entry resolves through the idm.relationships "
"capability at the expansion effective time. Effective identities with linked "
"accounts become internal-mail candidates. Future, expired, revoked, inactive, "
"and account-less relationships remain in exclusion evidence with source revisions "
"and provenance. Without IDM, the provider is reported as unavailable and local or "
"other provider-backed lists continue to work."
),
layer="available",
documentation_types=("admin", "user"),
audience=("operator", "module_admin", "product_owner"),
related_modules=("identity", "idm"),
metadata={"seed": True},
),
)
def _router(_context: ModuleContext):
from govoplan_dist_lists.backend.router import router
return router
def _capability(context: ModuleContext):
from govoplan_dist_lists.backend.capabilities import capability
return capability(context)
def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
return {
"distribution_lists": (
session.query(dist_list_models.DistributionList)
.filter(
dist_list_models.DistributionList.tenant_id == tenant_id,
dist_list_models.DistributionList.deleted_at.is_(None),
)
.count()
),
"distribution_list_snapshots": (
session.query(dist_list_models.DistributionListSnapshot)
.filter(dist_list_models.DistributionListSnapshot.tenant_id == tenant_id)
.count()
),
}
manifest = ModuleManifest(
id=MODULE_ID,
name=MODULE_NAME,
version=MODULE_VERSION,
dependencies=(),
optional_dependencies=(
"addresses",
"access",
"identity",
"idm",
"organizations",
"datasources",
"connectors",
"dataflow",
"policy",
"campaigns",
"templates",
"reporting",
"mail",
"postbox",
"notifications",
"scheduling",
"poll",
"workflow_engine",
"tasks",
),
optional_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_IDENTITY_DIRECTORY,
CAPABILITY_IDENTITY_SEARCH,
CAPABILITY_IDM_FUNCTION_ASSIGNMENTS,
CAPABILITY_IDM_RELATIONSHIPS,
CAPABILITY_ORGANIZATION_DIRECTORY,
CAPABILITY_DATAFLOW_DATASET_OUTPUT,
CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION,
CAPABILITY_RECIPIENT_CHANNEL_FACTS,
CAPABILITY_POLICY_DISTRIBUTION_CHANNELS,
),
provides_interfaces=(
ModuleInterfaceProvider(
name=CAPABILITY_DISTRIBUTION_LIST_SOURCE,
version=MODULE_VERSION,
),
ModuleInterfaceProvider(
name=CAPABILITY_DISTRIBUTION_LIST_EXPAND,
version=MODULE_VERSION,
),
ModuleInterfaceProvider(
name=CAPABILITY_DISTRIBUTION_LIST_WRITER,
version=MODULE_VERSION,
),
),
requires_interfaces=(
ModuleInterfaceRequirement(
name=CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION,
version_min="1.0.0",
version_max_exclusive="2.0.0",
optional=True,
),
ModuleInterfaceRequirement(
name=CAPABILITY_RECIPIENT_CHANNEL_FACTS,
version_min="0.1.0",
version_max_exclusive="0.2.0",
optional=True,
),
ModuleInterfaceRequirement(
name=CAPABILITY_IDM_RELATIONSHIPS,
version_min="1.0.0",
version_max_exclusive="2.0.0",
optional=True,
),
),
permissions=PERMISSIONS,
role_templates=ROLE_TEMPLATES,
nav_items=(
NavItem(
path="/distribution-lists",
label="Distribution Lists",
icon="list-tree",
required_any=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE),
order=74,
),
),
frontend=FrontendModule(
module_id=MODULE_ID,
package_name="@govoplan/dist-lists-webui",
routes=(
FrontendRoute(
path="/distribution-lists",
component="DistributionListsPage",
required_any=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE),
order=74,
),
),
nav_items=(
NavItem(
path="/distribution-lists",
label="Distribution Lists",
icon="list-tree",
required_any=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE),
order=74,
),
),
view_surfaces=(
ViewSurface(
id="dist_lists.page",
module_id=MODULE_ID,
kind="route",
label="Distribution Lists",
order=74,
),
ViewSurface(
id="dist_lists.editor",
module_id=MODULE_ID,
kind="section",
label="Distribution-list editor",
order=10,
),
ViewSurface(
id="dist_lists.preview",
module_id=MODULE_ID,
kind="section",
label="Distribution-list expansion preview",
order=20,
),
ViewSurface(
id="dist_lists.picker",
module_id=MODULE_ID,
kind="action",
label="Distribution-list picker",
order=30,
),
),
),
route_factory=_router,
capability_factories={
CAPABILITY_DISTRIBUTION_LIST_SOURCE: _capability,
CAPABILITY_DISTRIBUTION_LIST_EXPAND: _capability,
CAPABILITY_DISTRIBUTION_LIST_WRITER: _capability,
},
tenant_summary_providers=(_tenant_summary,),
migration_spec=MigrationSpec(
module_id=MODULE_ID,
metadata=Base.metadata,
script_location=str(Path(__file__).with_name("migrations") / "versions"),
retirement_supported=True,
retirement_provider=drop_table_retirement_provider(
dist_list_models.DistributionListSnapshot,
dist_list_models.DistributionListEntry,
dist_list_models.DistributionListRevision,
dist_list_models.DistributionList,
label=MODULE_NAME,
),
retirement_notes=(
"Destructive retirement drops distribution-list definitions, immutable revisions, "
"and frozen expansion evidence after the installer captures a database snapshot."
),
),
uninstall_guard_providers=(
persistent_table_uninstall_guard(
dist_list_models.DistributionList,
dist_list_models.DistributionListRevision,
dist_list_models.DistributionListEntry,
dist_list_models.DistributionListSnapshot,
label=MODULE_NAME,
),
),
documentation=DOCUMENTATION,
architecture=declared_module_architecture(
layer="communication_participation",
kind="domain",
maturity="vertical_slice",
documentation_ref="docs/DISTRIBUTION_LISTS_ARCHITECTURE.md",
test_ref="tests/test_service.py",
known_limits=("Portal self-service and every AdreMa migration path are not yet implemented.",),
supported_authority_modes=("native_authoritative", "external_mirror"),
owned_concepts=("distribution list", "distribution-list revision", "frozen expansion"),
non_owned_concepts=("contact point", "campaign recipient snapshot", "identity", "dataflow output"),
recovery_docs=("docs/DISTRIBUTION_LISTS_ARCHITECTURE.md",),
security_docs=("docs/DISTRIBUTION_LISTS_ARCHITECTURE.md",),
operations_docs=("README.md",),
),
)
def get_manifest() -> ModuleManifest:
return manifest