"""Registry of every ``audit_logs.action`` the mail-group domain writes. The 操作日志 tab renders a stable, translatable action filter. Deriving that list with ``SELECT DISTINCT action`` would be both slow (a distinct over a large append-only table) and *wrong in a specific way*: an action nobody has performed yet would be missing from the dropdown, so HR could never filter for it — and an action that stops being written would linger forever. A hand-kept registry makes the surface honest and gives each action a category the UI groups and colours by. **Keep this in sync** whenever an ``action="email_group.*"`` audit call lands. ``tests/test_email_group/test_manage_actions_registry.py`` greps the source for those literals and asserts *set equality* with this dict, so both a forgotten addition and a stale leftover fail CI rather than silently corrupting the filter. Categories: ``group`` — group lifecycle (create / delete / display / alias) ``member`` — membership + ownership changes ``request`` — the self-service join-request workflow ``dynamic`` — rule-driven group management ``sync`` — system-side reconciliation with Google ``cleanup`` — the bulk cleanup workbench ``export`` — read-only data egress """ from __future__ import annotations from typing import Final # action → category. Insertion order is the display order in the filter. EMAIL_GROUP_ACTIONS: Final[dict[str, str]] = { # ── group lifecycle ── "email_group.create": "group", "email_group.delete": "group", "email_group.display_updated": "group", "email_group.alias_added": "group", "email_group.alias_removed": "group", # ── membership / ownership ── "email_group.member_added": "member", "email_group.member_removed": "member", "email_group.owner_added": "member", "email_group.owner_mirror_repaired": "member", "email_group.owner_removed": "member", "email_group.transfer_owner": "member", "email_group.leave": "member", # ── join-request workflow ── "email_group.join_requested": "request", "email_group.join_approved": "request", "email_group.join_rejected": "request", "email_group.join_cancelled": "request", # ── dynamic groups ── "email_group.dynamic_created": "dynamic", "email_group.dynamic_rule_updated": "dynamic", "email_group.dynamic_display_updated": "dynamic", "email_group.dynamic_alias_added": "dynamic", "email_group.dynamic_alias_removed": "dynamic", "email_group.dynamic_missing_alert_set": "dynamic", "email_group.dynamic_deleted": "dynamic", # ── system sync with Google ── "email_group.sync_discovered": "sync", "email_group.sync_discovered_bulk": "sync", "email_group.sync_restored": "sync", "email_group.sync_restored_bulk": "sync", "email_group.sync_removed": "sync", "email_group.sync_renamed": "sync", "email_group.sync_owner_changed": "sync", "email_group.sync_dynamic_flag_changed": "sync", "email_group.sync_alias_changed": "sync", # ── cleanup workbench ── "email_group.cleanup_deleted": "cleanup", "email_group.cleanup_refused": "cleanup", "email_group.cleanup_batch": "cleanup", # ── exports ── "email_group.dynamic_members.export": "export", "email_group.groups.export": "export", "email_group.audit_log.export": "export", "email_group.cleanup_candidates.export": "export", } # Actions whose literal never appears in the source because the call # site builds them dynamically. The registry test skips these when # checking "every registered action is grepable". DYNAMICALLY_BUILT_ACTIONS: Final[frozenset[str]] = frozenset({ # service.decide_join → action=f"email_group.join_{req.status}" "email_group.join_approved", "email_group.join_rejected", }) def known_actions() -> list[str]: """Every mail-group audit action, in display order.""" return list(EMAIL_GROUP_ACTIONS) def category_of(action: str) -> str: """Category for ``action``; ``other`` for anything unregistered. Unregistered actions still RENDER — the log never hides a row just because the registry is stale; they only fall into ``other``. """ return EMAIL_GROUP_ACTIONS.get(action, "other")