refactor: remove deprecated menu_registry.py in favor of module-driven menus
- Delete app/config/menu_registry.py (centralized menu definitions) - Update app/config/__init__.py to remove menu_registry exports - Migrate menu_service.py to use menu_discovery_service exclusively - Add helper methods to menu_discovery_service.py: - get_all_menu_item_ids() - get all item IDs for a frontend type - get_menu_item() - get specific menu item by ID - is_super_admin_only_item() - check if item is super-admin only - get_menu_item_module() - get module code for a menu item Menu items are now defined in each module's definition.py file and aggregated by MenuDiscoveryService at runtime, enabling true module-driven architecture. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -372,6 +372,101 @@ class MenuDiscoveryService:
|
||||
|
||||
return mandatory_ids
|
||||
|
||||
def get_all_menu_item_ids(
|
||||
self,
|
||||
frontend_type: FrontendType,
|
||||
) -> set[str]:
|
||||
"""
|
||||
Get set of all menu item IDs for a frontend type.
|
||||
|
||||
Args:
|
||||
frontend_type: Frontend type to get item IDs for
|
||||
|
||||
Returns:
|
||||
Set of all menu item IDs from all modules
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
item_ids = set()
|
||||
|
||||
for module_def in MODULES.values():
|
||||
for section in module_def.menus.get(frontend_type, []):
|
||||
for item in section.items:
|
||||
item_ids.add(item.id)
|
||||
|
||||
return item_ids
|
||||
|
||||
def get_menu_item(
|
||||
self,
|
||||
frontend_type: FrontendType,
|
||||
menu_item_id: str,
|
||||
) -> DiscoveredMenuItem | None:
|
||||
"""
|
||||
Get a specific menu item by ID.
|
||||
|
||||
Args:
|
||||
frontend_type: Frontend type to search in
|
||||
menu_item_id: Menu item ID to look up
|
||||
|
||||
Returns:
|
||||
DiscoveredMenuItem if found, None otherwise
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
for module_code, module_def in MODULES.items():
|
||||
for section in module_def.menus.get(frontend_type, []):
|
||||
for item in section.items:
|
||||
if item.id == menu_item_id:
|
||||
return DiscoveredMenuItem(
|
||||
id=item.id,
|
||||
label_key=item.label_key,
|
||||
icon=item.icon,
|
||||
route=item.route,
|
||||
order=item.order,
|
||||
is_mandatory=item.is_mandatory,
|
||||
requires_permission=item.requires_permission,
|
||||
badge_source=item.badge_source,
|
||||
is_super_admin_only=item.is_super_admin_only,
|
||||
module_code=module_code,
|
||||
section_id=section.id,
|
||||
section_label_key=section.label_key,
|
||||
section_order=section.order,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def is_super_admin_only_item(
|
||||
self,
|
||||
menu_item_id: str,
|
||||
frontend_type: FrontendType = FrontendType.ADMIN,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a menu item is super-admin only.
|
||||
|
||||
An item is super-admin only if either:
|
||||
- The item itself has is_super_admin_only=True
|
||||
- The section containing the item has is_super_admin_only=True
|
||||
|
||||
Args:
|
||||
menu_item_id: Menu item ID to check
|
||||
frontend_type: Frontend type to search in
|
||||
|
||||
Returns:
|
||||
True if the item is super-admin only
|
||||
"""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
for module_def in MODULES.values():
|
||||
for section in module_def.menus.get(frontend_type, []):
|
||||
# Check if section is super-admin only
|
||||
section_is_super_admin = section.is_super_admin_only
|
||||
|
||||
for item in section.items:
|
||||
if item.id == menu_item_id:
|
||||
return section_is_super_admin or item.is_super_admin_only
|
||||
|
||||
return False
|
||||
|
||||
def get_menu_item_module(
|
||||
self,
|
||||
menu_item_id: str,
|
||||
|
||||
@@ -34,15 +34,8 @@ from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config.menu_registry import (
|
||||
ADMIN_MENU_REGISTRY,
|
||||
VENDOR_MENU_REGISTRY,
|
||||
get_all_menu_item_ids,
|
||||
get_menu_item,
|
||||
is_super_admin_only_item,
|
||||
)
|
||||
from app.modules.service import module_service
|
||||
from app.modules.core.models import AdminMenuConfig, MANDATORY_MENU_ITEMS
|
||||
from app.modules.core.models import AdminMenuConfig
|
||||
from app.modules.core.services.menu_discovery_service import menu_discovery_service
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
@@ -108,7 +101,7 @@ class MenuService:
|
||||
True if menu item is visible/accessible
|
||||
"""
|
||||
# Validate menu item exists in registry
|
||||
all_items = get_all_menu_item_ids(frontend_type)
|
||||
all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
|
||||
if menu_item_id not in all_items:
|
||||
logger.warning(f"Unknown menu item: {menu_item_id} for {frontend_type.value}")
|
||||
return False
|
||||
@@ -121,7 +114,8 @@ class MenuService:
|
||||
return False
|
||||
|
||||
# Mandatory items are always accessible (if module is enabled)
|
||||
if menu_item_id in MANDATORY_MENU_ITEMS.get(frontend_type, set()):
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)
|
||||
if menu_item_id in mandatory_items:
|
||||
return True
|
||||
|
||||
# No scope specified - show all by default (fallback for unconfigured)
|
||||
@@ -161,8 +155,8 @@ class MenuService:
|
||||
Returns:
|
||||
Set of visible menu item IDs
|
||||
"""
|
||||
all_items = get_all_menu_item_ids(frontend_type)
|
||||
mandatory_items = MANDATORY_MENU_ITEMS.get(frontend_type, set())
|
||||
all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)
|
||||
|
||||
# Filter by module enablement if platform is specified
|
||||
if platform_id:
|
||||
@@ -294,60 +288,47 @@ class MenuService:
|
||||
Returns:
|
||||
List of MenuItemConfig with current visibility state and module info
|
||||
"""
|
||||
from app.modules.registry import get_menu_item_module
|
||||
|
||||
registry = (
|
||||
ADMIN_MENU_REGISTRY if frontend_type == FrontendType.ADMIN else VENDOR_MENU_REGISTRY
|
||||
)
|
||||
|
||||
shown_items = self._get_shown_items(db, frontend_type, platform_id=platform_id)
|
||||
mandatory_items = MANDATORY_MENU_ITEMS.get(frontend_type, set())
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)
|
||||
|
||||
# Get module-available items
|
||||
all_item_ids = menu_discovery_service.get_all_menu_item_ids(frontend_type)
|
||||
module_available_items = module_service.filter_menu_items_by_modules(
|
||||
db, platform_id, get_all_menu_item_ids(frontend_type), frontend_type
|
||||
db, platform_id, all_item_ids, frontend_type
|
||||
)
|
||||
|
||||
# Get all menu items from discovery service
|
||||
all_items = menu_discovery_service.get_all_menu_items(frontend_type)
|
||||
|
||||
result = []
|
||||
for section in registry["sections"]:
|
||||
section_id = section["id"]
|
||||
section_label = section.get("label")
|
||||
is_super_admin_section = section.get("super_admin_only", False)
|
||||
for item in all_items:
|
||||
# Check if module is enabled for this item
|
||||
is_module_enabled = item.id in module_available_items
|
||||
|
||||
for item in section["items"]:
|
||||
item_id = item["id"]
|
||||
# If no config exists (shown_items is None), show all by default
|
||||
# Otherwise, item is visible if in shown_items or mandatory
|
||||
# Note: visibility config is independent of module enablement
|
||||
is_visible = (
|
||||
shown_items is None
|
||||
or item.id in shown_items
|
||||
or item.id in mandatory_items
|
||||
)
|
||||
|
||||
# Check if module is enabled for this item
|
||||
is_module_enabled = item_id in module_available_items
|
||||
module_code = get_menu_item_module(item_id, frontend_type)
|
||||
|
||||
# If no config exists (shown_items is None), show all by default
|
||||
# Otherwise, item is visible if in shown_items or mandatory
|
||||
# Note: visibility config is independent of module enablement
|
||||
is_visible = (
|
||||
shown_items is None
|
||||
or item_id in shown_items
|
||||
or item_id in mandatory_items
|
||||
)
|
||||
|
||||
# Item is super admin only if section or item is marked as such
|
||||
is_item_super_admin_only = is_super_admin_section or item.get("super_admin_only", False)
|
||||
|
||||
result.append(
|
||||
MenuItemConfig(
|
||||
id=item_id,
|
||||
label=item["label"],
|
||||
icon=item["icon"],
|
||||
url=item["url"],
|
||||
section_id=section_id,
|
||||
section_label=section_label,
|
||||
is_visible=is_visible,
|
||||
is_mandatory=item_id in mandatory_items,
|
||||
is_super_admin_only=is_item_super_admin_only,
|
||||
is_module_enabled=is_module_enabled,
|
||||
module_code=module_code,
|
||||
)
|
||||
result.append(
|
||||
MenuItemConfig(
|
||||
id=item.id,
|
||||
label=item.label_key,
|
||||
icon=item.icon,
|
||||
url=item.route,
|
||||
section_id=item.section_id,
|
||||
section_label=item.section_label_key,
|
||||
is_visible=is_visible,
|
||||
is_mandatory=item.id in mandatory_items,
|
||||
is_super_admin_only=item.is_super_admin_only,
|
||||
is_module_enabled=is_module_enabled,
|
||||
module_code=item.module_code,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -369,47 +350,39 @@ class MenuService:
|
||||
Returns:
|
||||
List of MenuItemConfig with current visibility state
|
||||
"""
|
||||
from app.modules.registry import get_menu_item_module
|
||||
|
||||
shown_items = self._get_shown_items(
|
||||
db, FrontendType.ADMIN, user_id=user_id
|
||||
)
|
||||
mandatory_items = MANDATORY_MENU_ITEMS.get(FrontendType.ADMIN, set())
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(FrontendType.ADMIN)
|
||||
|
||||
# Get all menu items from discovery service
|
||||
all_items = menu_discovery_service.get_all_menu_items(FrontendType.ADMIN)
|
||||
|
||||
result = []
|
||||
for section in ADMIN_MENU_REGISTRY["sections"]:
|
||||
section_id = section["id"]
|
||||
section_label = section.get("label")
|
||||
is_super_admin_section = section.get("super_admin_only", False)
|
||||
for item in all_items:
|
||||
# If no config exists (shown_items is None), show all by default
|
||||
# Otherwise, item is visible if in shown_items or mandatory
|
||||
is_visible = (
|
||||
shown_items is None
|
||||
or item.id in shown_items
|
||||
or item.id in mandatory_items
|
||||
)
|
||||
|
||||
for item in section["items"]:
|
||||
item_id = item["id"]
|
||||
module_code = get_menu_item_module(item_id, FrontendType.ADMIN)
|
||||
|
||||
# If no config exists (shown_items is None), show all by default
|
||||
# Otherwise, item is visible if in shown_items or mandatory
|
||||
is_visible = (
|
||||
shown_items is None
|
||||
or item_id in shown_items
|
||||
or item_id in mandatory_items
|
||||
)
|
||||
# Item is super admin only if section or item is marked as such
|
||||
is_item_super_admin_only = is_super_admin_section or item.get("super_admin_only", False)
|
||||
result.append(
|
||||
MenuItemConfig(
|
||||
id=item_id,
|
||||
label=item["label"],
|
||||
icon=item["icon"],
|
||||
url=item["url"],
|
||||
section_id=section_id,
|
||||
section_label=section_label,
|
||||
is_visible=is_visible,
|
||||
is_mandatory=item_id in mandatory_items,
|
||||
is_super_admin_only=is_item_super_admin_only,
|
||||
is_module_enabled=True, # Super admins see all modules
|
||||
module_code=module_code,
|
||||
)
|
||||
result.append(
|
||||
MenuItemConfig(
|
||||
id=item.id,
|
||||
label=item.label_key,
|
||||
icon=item.icon,
|
||||
url=item.route,
|
||||
section_id=item.section_id,
|
||||
section_label=item.section_label_key,
|
||||
is_visible=is_visible,
|
||||
is_mandatory=item.id in mandatory_items,
|
||||
is_super_admin_only=item.is_super_admin_only,
|
||||
is_module_enabled=True, # Super admins see all modules
|
||||
module_code=item.module_code,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -443,12 +416,12 @@ class MenuService:
|
||||
ValueError: If user_id is provided for vendor frontend
|
||||
"""
|
||||
# Validate menu item exists
|
||||
all_items = get_all_menu_item_ids(frontend_type)
|
||||
all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
|
||||
if menu_item_id not in all_items:
|
||||
raise ValueError(f"Unknown menu item: {menu_item_id}")
|
||||
|
||||
# Check if mandatory - mandatory items are always visible, no need to store
|
||||
mandatory = MANDATORY_MENU_ITEMS.get(frontend_type, set())
|
||||
mandatory = menu_discovery_service.get_mandatory_item_ids(frontend_type)
|
||||
if menu_item_id in mandatory:
|
||||
if not is_visible:
|
||||
raise ValueError(f"Cannot hide mandatory menu item: {menu_item_id}")
|
||||
@@ -559,8 +532,8 @@ class MenuService:
|
||||
|
||||
# Create records with is_visible=False for all non-mandatory items
|
||||
# This makes "reset" mean "hide everything except mandatory"
|
||||
all_items = get_all_menu_item_ids(frontend_type)
|
||||
mandatory_items = MANDATORY_MENU_ITEMS.get(frontend_type, set())
|
||||
all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)
|
||||
|
||||
for item_id in all_items:
|
||||
if item_id not in mandatory_items:
|
||||
@@ -603,8 +576,8 @@ class MenuService:
|
||||
logger.info(f"Reset menu config for user {user_id}: deleted {deleted} rows")
|
||||
|
||||
# Create records with is_visible=False for all non-mandatory items
|
||||
all_items = get_all_menu_item_ids(FrontendType.ADMIN)
|
||||
mandatory_items = MANDATORY_MENU_ITEMS.get(FrontendType.ADMIN, set())
|
||||
all_items = menu_discovery_service.get_all_menu_item_ids(FrontendType.ADMIN)
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(FrontendType.ADMIN)
|
||||
|
||||
for item_id in all_items:
|
||||
if item_id not in mandatory_items:
|
||||
@@ -650,8 +623,8 @@ class MenuService:
|
||||
)
|
||||
|
||||
# Create records with is_visible=True for all non-mandatory items
|
||||
all_items = get_all_menu_item_ids(frontend_type)
|
||||
mandatory_items = MANDATORY_MENU_ITEMS.get(frontend_type, set())
|
||||
all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)
|
||||
|
||||
for item_id in all_items:
|
||||
if item_id not in mandatory_items:
|
||||
@@ -692,8 +665,8 @@ class MenuService:
|
||||
logger.info(f"Show all menu config for user {user_id}: deleted {deleted} rows")
|
||||
|
||||
# Create records with is_visible=True for all non-mandatory items
|
||||
all_items = get_all_menu_item_ids(FrontendType.ADMIN)
|
||||
mandatory_items = MANDATORY_MENU_ITEMS.get(FrontendType.ADMIN, set())
|
||||
all_items = menu_discovery_service.get_all_menu_item_ids(FrontendType.ADMIN)
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(FrontendType.ADMIN)
|
||||
|
||||
for item_id in all_items:
|
||||
if item_id not in mandatory_items:
|
||||
@@ -762,8 +735,8 @@ class MenuService:
|
||||
logger.info(f"Cleaned up {deleted} old menu config records before initialization")
|
||||
|
||||
# Get all menu items for this frontend
|
||||
all_items = get_all_menu_item_ids(frontend_type)
|
||||
mandatory_items = MANDATORY_MENU_ITEMS.get(frontend_type, set())
|
||||
all_items = menu_discovery_service.get_all_menu_item_ids(frontend_type)
|
||||
mandatory_items = menu_discovery_service.get_mandatory_item_ids(frontend_type)
|
||||
|
||||
# Create visible records for all non-mandatory items
|
||||
for item_id in all_items:
|
||||
|
||||
Reference in New Issue
Block a user