"""Optional-app registry. Apps register from AppConfig.ready() — never from always-on imports.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Callable from django.urls import reverse @dataclass(frozen=True) class NavItem: section: str label: str url_name: str group: str = "Overview" order: int = 100 GROUP_ORDER = { "Overview": 10, "Outreach": 20, "Content": 30, "Social": 40, "Billing": 50, } _portal_nav: list[NavItem] = [] _public_nav: list[NavItem] = [] _dashboard_collectors: list[Callable] = [] _dispatchers: list[Callable] = [] _features: set[str] = set() def register_feature(name: str) -> None: _features.add(name) def enabled_features() -> frozenset[str]: return frozenset(_features) def register_portal_nav( *, section: str, label: str, url_name: str, group: str = "Overview", order: int = 100, ) -> None: item = NavItem( section=section, label=label, url_name=url_name, group=group, order=order ) _portal_nav.append(item) def register_public_nav( *, section: str, label: str, url_name: str, order: int = 100, ) -> None: _public_nav.append( NavItem(section=section, label=label, url_name=url_name, order=order) ) def register_dashboard_collector(fn: Callable) -> None: """fn(request) -> dict merged into dashboard home context.""" _dashboard_collectors.append(fn) def register_dispatcher(fn: Callable) -> None: """fn() -> int count of items enqueued. Called by dispatch_due.""" _dispatchers.append(fn) def portal_nav() -> list[NavItem]: return sorted( _portal_nav, key=lambda i: (GROUP_ORDER.get(i.group, 99), i.order, i.label), ) def public_nav() -> list[NavItem]: return sorted(_public_nav, key=lambda i: (i.order, i.label)) def collect_dashboard_context(request) -> dict: extra: dict = {} for fn in _dashboard_collectors: extra.update(fn(request) or {}) return extra def run_dispatchers() -> int: total = 0 for fn in _dispatchers: total += int(fn() or 0) return total def reverse_nav(item: NavItem) -> str: return reverse(item.url_name)