This commit is contained in:
@@ -12,7 +12,7 @@ class SystemSettings(Base, TimestampMixin):
|
||||
__tablename__ = "core_system_settings"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default="global")
|
||||
default_locale: Mapped[str] = mapped_column(String(20), default="en", nullable=False)
|
||||
default_locale: Mapped[str] = mapped_column(String(20), default="de", nullable=False)
|
||||
allow_tenant_custom_groups: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
allow_tenant_custom_roles: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
allow_tenant_api_keys: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
@@ -20,4 +20,3 @@ class SystemSettings(Base, TimestampMixin):
|
||||
|
||||
|
||||
__all__ = ["SystemSettings"]
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class TenantInfo(BaseModel):
|
||||
slug: str
|
||||
name: str
|
||||
is_active: bool = True
|
||||
default_locale: str = "en"
|
||||
default_locale: str = "de"
|
||||
enabled_language_codes: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ class AuthProfileResponse(BaseModel):
|
||||
active_tenant: TenantInfo
|
||||
available_languages: list[LanguageInfo] = Field(default_factory=list)
|
||||
enabled_language_codes: list[str] = Field(default_factory=list)
|
||||
default_language: str = "en"
|
||||
default_language: str = "de"
|
||||
profile_loaded: bool = True
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ class LoginResponse(BaseModel):
|
||||
principal: PrincipalContextInfo | None = None
|
||||
available_languages: list[LanguageInfo] = Field(default_factory=list)
|
||||
enabled_language_codes: list[str] = Field(default_factory=list)
|
||||
default_language: str = "en"
|
||||
default_language: str = "de"
|
||||
profile_loaded: bool = True
|
||||
roles_loaded: bool = True
|
||||
groups_loaded: bool = True
|
||||
@@ -257,7 +257,7 @@ class MeResponse(BaseModel):
|
||||
principal: PrincipalContextInfo | None = None
|
||||
available_languages: list[LanguageInfo] = Field(default_factory=list)
|
||||
enabled_language_codes: list[str] = Field(default_factory=list)
|
||||
default_language: str = "en"
|
||||
default_language: str = "de"
|
||||
profile_loaded: bool = True
|
||||
roles_loaded: bool = True
|
||||
groups_loaded: bool = True
|
||||
|
||||
@@ -356,7 +356,7 @@ def consume_first_admin_credential(
|
||||
tenant = Tenant(
|
||||
slug=clean_tenant_slug,
|
||||
name=clean_tenant_name,
|
||||
default_locale="en",
|
||||
default_locale="de",
|
||||
settings={},
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal, Mapping, cast
|
||||
|
||||
|
||||
InformationGovernanceAdoption = Literal[
|
||||
"not_applicable",
|
||||
"contract_only",
|
||||
"partial",
|
||||
"enforced",
|
||||
]
|
||||
|
||||
INFORMATION_GOVERNANCE_ADOPTION_ORDER: tuple[InformationGovernanceAdoption, ...] = (
|
||||
"not_applicable",
|
||||
"contract_only",
|
||||
"partial",
|
||||
"enforced",
|
||||
)
|
||||
|
||||
|
||||
class InformationGovernanceDeclarationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InformationGovernanceDimension:
|
||||
"""Truthful module-level adoption claim for one cross-cutting dimension."""
|
||||
|
||||
adoption: InformationGovernanceAdoption = "contract_only"
|
||||
object_types: tuple[str, ...] = ()
|
||||
evidence: tuple[str, ...] = ()
|
||||
limitation: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.adoption not in INFORMATION_GOVERNANCE_ADOPTION_ORDER:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Unsupported information-governance adoption: {self.adoption!r}."
|
||||
)
|
||||
for field_name in ("object_types", "evidence"):
|
||||
values = getattr(self, field_name)
|
||||
if len(values) != len(set(values)) or any(not item.strip() for item in values):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {field_name.replace('_', ' ')} must "
|
||||
"contain unique non-empty values."
|
||||
)
|
||||
if self.adoption == "enforced" and not self.evidence:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"An enforced information-governance dimension requires evidence."
|
||||
)
|
||||
if self.adoption in {"partial", "enforced"} and not self.object_types:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Partial and enforced information-governance dimensions must "
|
||||
"name their covered object types."
|
||||
)
|
||||
if self.adoption == "not_applicable" and self.object_types:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"A non-applicable information-governance dimension cannot declare object types."
|
||||
)
|
||||
if self.adoption in {"contract_only", "partial"} and not str(
|
||||
self.limitation or ""
|
||||
).strip():
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Contract-only and partial adoption must state the current limitation."
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"adoption": self.adoption,
|
||||
"object_types": list(self.object_types),
|
||||
"evidence": list(self.evidence),
|
||||
"limitation": self.limitation,
|
||||
}
|
||||
|
||||
|
||||
def _contract_only_dimension() -> InformationGovernanceDimension:
|
||||
return InformationGovernanceDimension(
|
||||
adoption="contract_only",
|
||||
limitation=(
|
||||
"The platform contract applies, but module-specific adoption evidence "
|
||||
"has not been declared."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ModuleInformationGovernance:
|
||||
"""Cross-cutting data-use requirements and honest adoption evidence."""
|
||||
|
||||
temporal_browsing: InformationGovernanceDimension = field(
|
||||
default_factory=_contract_only_dimension
|
||||
)
|
||||
purpose_aware_access: InformationGovernanceDimension = field(
|
||||
default_factory=_contract_only_dimension
|
||||
)
|
||||
retention: InformationGovernanceDimension = field(
|
||||
default_factory=_contract_only_dimension
|
||||
)
|
||||
institutional_context: InformationGovernanceDimension = field(
|
||||
default_factory=_contract_only_dimension
|
||||
)
|
||||
current_authorization_for_historical_reads: bool = True
|
||||
contract_version: str = "1"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.contract_version != "1":
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Unsupported module information-governance contract version."
|
||||
)
|
||||
if not self.current_authorization_for_historical_reads:
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Historical reads must always use current authorization."
|
||||
)
|
||||
for name, dimension in self.dimensions.items():
|
||||
if not isinstance(dimension, InformationGovernanceDimension):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance dimension {name!r} has an invalid value."
|
||||
)
|
||||
|
||||
@property
|
||||
def dimensions(self) -> Mapping[str, InformationGovernanceDimension]:
|
||||
return {
|
||||
"temporal_browsing": self.temporal_browsing,
|
||||
"purpose_aware_access": self.purpose_aware_access,
|
||||
"retention": self.retention,
|
||||
"institutional_context": self.institutional_context,
|
||||
}
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"contract_version": self.contract_version,
|
||||
"current_authorization_for_historical_reads": (
|
||||
self.current_authorization_for_historical_reads
|
||||
),
|
||||
"dimensions": {
|
||||
name: dimension.to_dict()
|
||||
for name, dimension in self.dimensions.items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def information_governance_from_mapping(
|
||||
value: Mapping[str, object],
|
||||
) -> ModuleInformationGovernance:
|
||||
raw_dimensions = value.get("dimensions")
|
||||
if not isinstance(raw_dimensions, Mapping):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"Information-governance dimensions must be an object."
|
||||
)
|
||||
|
||||
def dimension(name: str) -> InformationGovernanceDimension:
|
||||
raw_dimension = raw_dimensions.get(name)
|
||||
if not isinstance(raw_dimension, Mapping):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance dimension {name!r} must be an object."
|
||||
)
|
||||
|
||||
def text_tuple(field_name: str) -> tuple[str, ...]:
|
||||
raw_values = raw_dimension.get(field_name, ())
|
||||
if not isinstance(raw_values, (list, tuple)):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {name}.{field_name} must be a list."
|
||||
)
|
||||
if any(not isinstance(item, str) for item in raw_values):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {name}.{field_name} must contain strings."
|
||||
)
|
||||
return tuple(raw_values)
|
||||
|
||||
raw_limitation = raw_dimension.get("limitation")
|
||||
raw_adoption = raw_dimension.get("adoption") or "contract_only"
|
||||
if not isinstance(raw_adoption, str):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {name}.adoption must be a string."
|
||||
)
|
||||
if raw_limitation is not None and not isinstance(raw_limitation, str):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
f"Information-governance {name}.limitation must be a string."
|
||||
)
|
||||
return InformationGovernanceDimension(
|
||||
adoption=cast(
|
||||
InformationGovernanceAdoption,
|
||||
raw_adoption,
|
||||
),
|
||||
object_types=text_tuple("object_types"),
|
||||
evidence=text_tuple("evidence"),
|
||||
limitation=raw_limitation,
|
||||
)
|
||||
|
||||
current_authorization = value.get(
|
||||
"current_authorization_for_historical_reads",
|
||||
True,
|
||||
)
|
||||
if not isinstance(current_authorization, bool):
|
||||
raise InformationGovernanceDeclarationError(
|
||||
"current_authorization_for_historical_reads must be boolean."
|
||||
)
|
||||
return ModuleInformationGovernance(
|
||||
contract_version=str(value.get("contract_version") or "1"),
|
||||
current_authorization_for_historical_reads=current_authorization,
|
||||
temporal_browsing=dimension("temporal_browsing"),
|
||||
purpose_aware_access=dimension("purpose_aware_access"),
|
||||
retention=dimension("retention"),
|
||||
institutional_context=dimension("institutional_context"),
|
||||
)
|
||||
|
||||
|
||||
def information_governance_maturity_issues(
|
||||
declaration: ModuleInformationGovernance,
|
||||
*,
|
||||
maturity: str | None,
|
||||
) -> tuple[str, ...]:
|
||||
if maturity not in {"reference_ready", "supported", "lts"}:
|
||||
return ()
|
||||
incomplete = [
|
||||
name
|
||||
for name, dimension in declaration.dimensions.items()
|
||||
if dimension.adoption not in {"not_applicable", "enforced"}
|
||||
]
|
||||
if not incomplete:
|
||||
return ()
|
||||
return (
|
||||
f"Maturity {maturity!r} requires enforced or explicitly non-applicable "
|
||||
"information governance for: " + ", ".join(incomplete),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"INFORMATION_GOVERNANCE_ADOPTION_ORDER",
|
||||
"InformationGovernanceAdoption",
|
||||
"InformationGovernanceDeclarationError",
|
||||
"InformationGovernanceDimension",
|
||||
"ModuleInformationGovernance",
|
||||
"information_governance_from_mapping",
|
||||
"information_governance_maturity_issues",
|
||||
]
|
||||
@@ -17,6 +17,10 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
|
||||
from govoplan_core.core.versioning import format_version_range, version_range_is_valid, version_satisfies_range
|
||||
from govoplan_core.core.information_governance import (
|
||||
information_governance_from_mapping,
|
||||
information_governance_maturity_issues,
|
||||
)
|
||||
from govoplan_core.core.provider_governance import (
|
||||
external_provider_from_mapping,
|
||||
module_architecture_from_mapping,
|
||||
@@ -630,6 +634,7 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]:
|
||||
"tags": _string_list(value.get("tags")),
|
||||
}
|
||||
raw_architecture = value.get("architecture")
|
||||
architecture_maturity: str | None = None
|
||||
if raw_architecture is not None:
|
||||
if not isinstance(raw_architecture, Mapping):
|
||||
raise ValueError(
|
||||
@@ -647,6 +652,27 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]:
|
||||
+ "; ".join(issues)
|
||||
)
|
||||
item["architecture"] = architecture.to_dict()
|
||||
architecture_maturity = architecture.maturity
|
||||
raw_information_governance = value.get("information_governance")
|
||||
if raw_information_governance is not None:
|
||||
if not isinstance(raw_information_governance, Mapping):
|
||||
raise ValueError(
|
||||
"Module package catalog information_governance for "
|
||||
f"{module_id!r} must be an object."
|
||||
)
|
||||
information_governance = information_governance_from_mapping(
|
||||
raw_information_governance
|
||||
)
|
||||
governance_issues = information_governance_maturity_issues(
|
||||
information_governance,
|
||||
maturity=architecture_maturity,
|
||||
)
|
||||
if governance_issues:
|
||||
raise ValueError(
|
||||
"Module package catalog information_governance for "
|
||||
f"{module_id!r} is invalid: " + "; ".join(governance_issues)
|
||||
)
|
||||
item["information_governance"] = information_governance.to_dict()
|
||||
raw_providers = value.get("external_providers")
|
||||
if raw_providers is not None:
|
||||
if not isinstance(raw_providers, list):
|
||||
|
||||
@@ -4,6 +4,7 @@ from collections.abc import Callable, Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Literal, Protocol, TYPE_CHECKING
|
||||
|
||||
from govoplan_core.core.information_governance import ModuleInformationGovernance
|
||||
from govoplan_core.core.ownership import OwnershipProviderRegistration
|
||||
from govoplan_core.core.provider_governance import (
|
||||
ExternalProviderDeclaration,
|
||||
@@ -446,6 +447,9 @@ class ModuleManifest:
|
||||
...,
|
||||
] = ()
|
||||
architecture: ModuleArchitectureDeclaration | None = None
|
||||
information_governance: ModuleInformationGovernance = field(
|
||||
default_factory=ModuleInformationGovernance
|
||||
)
|
||||
external_providers: tuple[ExternalProviderDeclaration, ...] = ()
|
||||
external_provider_state_providers: tuple[
|
||||
ExternalProviderStateProviderRegistration,
|
||||
|
||||
@@ -32,6 +32,9 @@ from govoplan_core.core.module_entitlements import (
|
||||
current_tenant_execution_context,
|
||||
tenant_execution_scope,
|
||||
)
|
||||
from govoplan_core.core.information_governance import (
|
||||
information_governance_maturity_issues,
|
||||
)
|
||||
from govoplan_core.core.ownership import (
|
||||
OwnershipProviderRegistration,
|
||||
ResourceOwnershipProvider,
|
||||
@@ -788,6 +791,13 @@ def _validate_architecture_declarations(manifest: ModuleManifest) -> None:
|
||||
raise RegistryError(
|
||||
f"Module {manifest.id!r} architecture declaration: {issue}"
|
||||
)
|
||||
for issue in information_governance_maturity_issues(
|
||||
manifest.information_governance,
|
||||
maturity=architecture.maturity if architecture is not None else None,
|
||||
):
|
||||
raise RegistryError(
|
||||
f"Module {manifest.id!r} information-governance declaration: {issue}"
|
||||
)
|
||||
|
||||
provider_ids: set[str] = set()
|
||||
declared_capabilities = {
|
||||
|
||||
@@ -73,7 +73,7 @@ def bootstrap_dev_data(
|
||||
) -> BootstrapResult:
|
||||
tenant = session.query(Tenant).filter(Tenant.slug == tenant_slug).one_or_none()
|
||||
if tenant is None:
|
||||
tenant = Tenant(slug=tenant_slug, name="Default Tenant", default_locale="en", settings={})
|
||||
tenant = Tenant(slug=tenant_slug, name="Default Tenant", default_locale="de", settings={})
|
||||
session.add(tenant)
|
||||
session.flush()
|
||||
|
||||
|
||||
@@ -685,7 +685,10 @@ def configured_migration_heads(
|
||||
manifest_factories=manifest_factories,
|
||||
migration_track=migration_track,
|
||||
)
|
||||
return tuple(sorted(ScriptDirectory.from_config(config).get_heads()))
|
||||
scripts = ScriptDirectory.from_config(config)
|
||||
return tuple(
|
||||
sorted(revision.revision for revision in scripts.get_revisions("heads"))
|
||||
)
|
||||
|
||||
|
||||
def database_is_at_configured_heads(
|
||||
|
||||
@@ -4,10 +4,12 @@ import re
|
||||
from typing import Any, Iterable
|
||||
|
||||
I18N_SETTINGS_KEY = "i18n"
|
||||
REFERENCE_LANGUAGE_CODE = "de"
|
||||
SOURCE_LANGUAGE_CODE = "en"
|
||||
|
||||
DEFAULT_LANGUAGE_PACKAGES: tuple[dict[str, str], ...] = (
|
||||
{"code": "en", "label": "English", "native_label": "English"},
|
||||
{"code": "de", "label": "German", "native_label": "Deutsch"},
|
||||
{"code": "en", "label": "English", "native_label": "English"},
|
||||
)
|
||||
|
||||
|
||||
@@ -66,7 +68,7 @@ def normalize_enabled_language_codes(
|
||||
available_languages: Iterable[dict[str, Any]],
|
||||
*,
|
||||
default_locale: object = None,
|
||||
fallback_codes: Iterable[str] = ("en", "de"),
|
||||
fallback_codes: Iterable[str] = (REFERENCE_LANGUAGE_CODE, SOURCE_LANGUAGE_CODE),
|
||||
) -> list[str]:
|
||||
available_codes = [normalize_language_code(item.get("code")) for item in available_languages if isinstance(item, dict)]
|
||||
available = {code for code in available_codes if code}
|
||||
@@ -88,8 +90,10 @@ def normalize_enabled_language_codes(
|
||||
|
||||
if enabled:
|
||||
return enabled
|
||||
if "en" in available:
|
||||
return ["en"]
|
||||
if REFERENCE_LANGUAGE_CODE in available:
|
||||
return [REFERENCE_LANGUAGE_CODE]
|
||||
if SOURCE_LANGUAGE_CODE in available:
|
||||
return [SOURCE_LANGUAGE_CODE]
|
||||
return available_codes[:1]
|
||||
|
||||
|
||||
@@ -126,7 +130,7 @@ def preferred_language_code(user_settings: dict[str, Any] | None, allowed_codes:
|
||||
default = resolve_language_code(default_locale, enabled)
|
||||
if default:
|
||||
return default
|
||||
return enabled[0] if enabled else "en"
|
||||
return enabled[0] if enabled else REFERENCE_LANGUAGE_CODE
|
||||
|
||||
|
||||
def update_i18n_settings(settings: dict[str, Any] | None, **values: Any) -> dict[str, Any]:
|
||||
@@ -141,10 +145,13 @@ def system_i18n_payload(settings_item: Any | None) -> dict[str, object]:
|
||||
settings = getattr(settings_item, "settings", None)
|
||||
packages = system_language_packages(settings)
|
||||
available_codes = [item["code"] for item in packages]
|
||||
default_language = resolve_language_code(getattr(settings_item, "default_locale", None), available_codes) or "en"
|
||||
default_language = (
|
||||
resolve_language_code(getattr(settings_item, "default_locale", None), available_codes)
|
||||
or REFERENCE_LANGUAGE_CODE
|
||||
)
|
||||
enabled = system_enabled_language_codes(settings, default_locale=default_language)
|
||||
if default_language not in enabled:
|
||||
default_language = enabled[0] if enabled else "en"
|
||||
default_language = enabled[0] if enabled else REFERENCE_LANGUAGE_CODE
|
||||
return {
|
||||
"available_languages": packages,
|
||||
"enabled_languages": enabled,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi import HTTPException, status
|
||||
@@ -20,6 +21,13 @@ from govoplan_core.core.runtime_coordination import (
|
||||
from govoplan_core.settings import Settings, settings
|
||||
|
||||
|
||||
def _core_distribution_version() -> str:
|
||||
try:
|
||||
return version("govoplan-core")
|
||||
except PackageNotFoundError:
|
||||
return "development"
|
||||
|
||||
|
||||
def _dev_bootstrap_needs_create_all(database_url: str) -> bool:
|
||||
try:
|
||||
return make_url(database_url).get_backend_name() == "sqlite"
|
||||
@@ -61,7 +69,7 @@ async def lifespan(app: FastAPI):
|
||||
)
|
||||
runtime_agent = RuntimeNodeAgent(
|
||||
settings=settings,
|
||||
software_version=app.version,
|
||||
software_version=_core_distribution_version(),
|
||||
module_ids=module_ids,
|
||||
metadata={"process": "api"},
|
||||
identity=configured_identity
|
||||
@@ -93,7 +101,7 @@ def register_health_details(
|
||||
):
|
||||
app.state.govoplan_runtime_identity = runtime_identity(
|
||||
active_settings,
|
||||
software_version=app.version,
|
||||
software_version=_core_distribution_version(),
|
||||
module_ids=module_ids,
|
||||
)
|
||||
bind_process_runtime_identity(app.state.govoplan_runtime_identity)
|
||||
|
||||
@@ -142,23 +142,58 @@ def _frontend_view_surfaces(manifest: ModuleManifest) -> list[dict[str, object]]
|
||||
def _documentation_help_contexts(manifest: ModuleManifest) -> list[dict[str, object]]:
|
||||
contexts: list[dict[str, object]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def append_context(context_id: str, topic) -> None:
|
||||
if not context_id or context_id in seen:
|
||||
return
|
||||
seen.add(context_id)
|
||||
contexts.append(
|
||||
{
|
||||
"id": context_id,
|
||||
"topic_id": topic.id,
|
||||
"title": topic.title,
|
||||
"documentation_types": list(topic.documentation_types),
|
||||
}
|
||||
)
|
||||
|
||||
for topic in manifest.documentation:
|
||||
raw_contexts = topic.metadata.get("help_contexts", ())
|
||||
if isinstance(raw_contexts, str) or not isinstance(raw_contexts, (list, tuple, set)):
|
||||
continue
|
||||
for raw_context in raw_contexts:
|
||||
context_id = str(raw_context).strip()
|
||||
if not context_id or context_id in seen:
|
||||
continue
|
||||
seen.add(context_id)
|
||||
contexts.append(
|
||||
{
|
||||
"id": context_id,
|
||||
"topic_id": topic.id,
|
||||
"title": topic.title,
|
||||
"documentation_types": list(topic.documentation_types),
|
||||
}
|
||||
)
|
||||
append_context(context_id, topic)
|
||||
|
||||
frontend = manifest.frontend
|
||||
if frontend is None or not manifest.documentation:
|
||||
return contexts
|
||||
|
||||
ordered_topics = sorted(manifest.documentation, key=lambda item: (item.order, item.id))
|
||||
|
||||
def baseline_topic(documentation_type: str):
|
||||
return next(
|
||||
(
|
||||
topic
|
||||
for topic in ordered_topics
|
||||
if documentation_type in topic.documentation_types
|
||||
),
|
||||
ordered_topics[0],
|
||||
)
|
||||
|
||||
user_topic = baseline_topic("user")
|
||||
admin_topic = baseline_topic("admin")
|
||||
for route in frontend.routes:
|
||||
if route.surface_id:
|
||||
append_context(route.surface_id, user_topic)
|
||||
for route in frontend.settings_routes:
|
||||
if route.surface_id:
|
||||
append_context(route.surface_id, admin_topic)
|
||||
for item in frontend.nav_items:
|
||||
if item.surface_id:
|
||||
append_context(item.surface_id, user_topic)
|
||||
for surface in frontend.view_surfaces:
|
||||
topic = admin_topic if ".admin." in surface.id else user_topic
|
||||
append_context(surface.id, topic)
|
||||
return contexts
|
||||
|
||||
|
||||
@@ -253,6 +288,7 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
|
||||
if manifest.architecture is not None
|
||||
else None
|
||||
),
|
||||
"information_governance": manifest.information_governance.to_dict(),
|
||||
"external_providers": [
|
||||
declaration.to_dict()
|
||||
for declaration in manifest.external_providers
|
||||
|
||||
@@ -33,7 +33,7 @@ class Tenant:
|
||||
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
default_locale: Mapped[str] = mapped_column(String(20), default="en", nullable=False)
|
||||
default_locale: Mapped[str] = mapped_column(String(20), default="de", nullable=False)
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
allow_custom_groups: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
allow_custom_roles: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
|
||||
Reference in New Issue
Block a user