""" Custom AdminSite for IGNY8 to organize models into proper groups using Unfold NO EMOJIS - Unfold handles all icons via Material Design """ from django.contrib import admin from django.contrib.admin.apps import AdminConfig from django.apps import apps from django.urls import path, reverse_lazy from django.shortcuts import redirect from django.contrib.admin import sites from unfold.admin import ModelAdmin as UnfoldModelAdmin from unfold.sites import UnfoldAdminSite class Igny8AdminSite(UnfoldAdminSite): """ Custom AdminSite based on Unfold that organizes models into the planned groups """ site_header = 'IGNY8 Administration' site_title = 'IGNY8 Admin' index_title = 'IGNY8 Administration' def get_urls(self): """Get admin URLs with dashboard, reports, and monitoring pages available""" from django.urls import path from .dashboard import admin_dashboard from .reports import ( revenue_report, usage_report, content_report, data_quality_report, token_usage_report, ai_cost_analysis ) from .monitoring import ( system_health_dashboard, api_monitor_dashboard, debug_console ) urls = super().get_urls() custom_urls = [ # Dashboard path('dashboard/', self.admin_view(admin_dashboard), name='dashboard'), # Reports path('reports/revenue/', self.admin_view(revenue_report), name='report_revenue'), path('reports/usage/', self.admin_view(usage_report), name='report_usage'), path('reports/content/', self.admin_view(content_report), name='report_content'), path('reports/data-quality/', self.admin_view(data_quality_report), name='report_data_quality'), path('reports/token-usage/', self.admin_view(token_usage_report), name='report_token_usage'), path('reports/ai-cost-analysis/', self.admin_view(ai_cost_analysis), name='report_ai_cost_analysis'), # Monitoring (NEW) path('monitoring/system-health/', self.admin_view(system_health_dashboard), name='monitoring_system_health'), path('monitoring/api-monitor/', self.admin_view(api_monitor_dashboard), name='monitoring_api_monitor'), path('monitoring/debug-console/', self.admin_view(debug_console), name='monitoring_debug_console'), ] return custom_urls + urls def index(self, request, extra_context=None): """Redirect to custom dashboard""" from django.shortcuts import redirect return redirect('admin:dashboard') def get_sidebar_list(self, request): """ Override Unfold's get_sidebar_list to return our custom app groups Convert Django app_list format to Unfold sidebar navigation format """ # Get our custom Django app list django_apps = self.get_app_list(request, app_label=None) # Convert to Unfold navigation format: {title, items: [{title, link, icon}]} sidebar_groups = [] for app in django_apps: group = { 'title': app['name'], 'collapsible': True, 'items': [] } # Convert each model to navigation item for model in app.get('models', []): if model.get('perms', {}).get('view', False) or model.get('perms', {}).get('change', False): item = { 'title': model['name'], 'link': model['admin_url'], 'icon': None, # Unfold will use default 'has_permission': True, # CRITICAL: Template checks this } group['items'].append(item) # Only add groups that have items if group['items']: sidebar_groups.append(group) return sidebar_groups def each_context(self, request): """ Override context to ensure our custom app_list is always used This is called by all admin templates for sidebar rendering CRITICAL FIX: Force custom sidebar on ALL pages including model detail/list views """ # CRITICAL: Must call parent to get sidebar_navigation set context = super().each_context(request) # DEBUGGING: Print to console what parent returned print(f"\n=== DEBUG each_context for {request.path} ===") print(f"sidebar_navigation length from parent: {len(context.get('sidebar_navigation', []))}") if context.get('sidebar_navigation'): print(f"First sidebar group: {context['sidebar_navigation'][0].get('title', 'NO TITLE')}") # Force our custom app list to be used everywhere - IGNORE app_label parameter custom_apps = self.get_app_list(request, app_label=None) context['available_apps'] = custom_apps context['app_list'] = custom_apps # Also set app_list for compatibility # CRITICAL FIX: Ensure sidebar_navigation is using our custom sidebar # Parent's each_context already called get_sidebar_list(), which returns our custom sidebar # So sidebar_navigation should already be correct, but let's verify if not context.get('sidebar_navigation') or len(context.get('sidebar_navigation', [])) == 0: # If sidebar_navigation is empty, force it print("WARNING: sidebar_navigation was empty, forcing it!") context['sidebar_navigation'] = self.get_sidebar_list(request) print(f"Final sidebar_navigation length: {len(context['sidebar_navigation'])}") print("=== END DEBUG ===\n") return context def get_app_list(self, request, app_label=None): """ Customize the app list to organize models into logical groups NO EMOJIS - Unfold handles all icons via Material Design Args: request: The HTTP request app_label: IGNORED - Always return full custom sidebar for consistency """ # CRITICAL: Always build full app_dict (ignore app_label) for consistent sidebar app_dict = self._build_app_dict(request, None) # Define our custom groups with their models (using object_name) # Organized by business function - Material icons configured in Unfold custom_groups = { 'Accounts & Tenancy': { 'models': [ ('igny8_core_auth', 'Account'), ('igny8_core_auth', 'User'), ('igny8_core_auth', 'Site'), ('igny8_core_auth', 'Sector'), ('igny8_core_auth', 'SiteUserAccess'), ], }, 'Global Resources': { 'models': [ ('igny8_core_auth', 'Industry'), ('igny8_core_auth', 'IndustrySector'), ('igny8_core_auth', 'SeedKeyword'), ], }, 'Global Settings': { 'models': [ ('system', 'GlobalIntegrationSettings'), ('system', 'GlobalModuleSettings'), ('billing', 'AIModelConfig'), ('system', 'GlobalAIPrompt'), ('system', 'GlobalAuthorProfile'), ('system', 'GlobalStrategy'), ], }, 'Plans and Billing': { 'models': [ ('igny8_core_auth', 'Plan'), ('igny8_core_auth', 'Subscription'), ('billing', 'BillingConfiguration'), ('billing', 'Invoice'), ('billing', 'Payment'), ('billing', 'CreditPackage'), ('billing', 'PaymentMethodConfig'), ('billing', 'AccountPaymentMethod'), ], }, 'Credits': { 'models': [ ('billing', 'CreditTransaction'), ('billing', 'CreditUsageLog'), ('billing', 'CreditCostConfig'), ('billing', 'PlanLimitUsage'), ], }, 'Content Planning': { 'models': [ ('planner', 'Keywords'), ('planner', 'Clusters'), ('planner', 'ContentIdeas'), ], }, 'Content Generation': { 'models': [ ('writer', 'Tasks'), ('writer', 'Content'), ('writer', 'Images'), ('writer', 'ImagePrompts'), ], }, 'Taxonomy & Organization': { 'models': [ ('writer', 'ContentTaxonomy'), ('writer', 'ContentTaxonomyRelation'), ('writer', 'ContentClusterMap'), ('writer', 'ContentAttribute'), ], }, 'Publishing & Integration': { 'models': [ ('integration', 'SiteIntegration'), ('integration', 'SyncEvent'), ('publishing', 'PublishingRecord'), ('system', 'PublishingChannel'), ('publishing', 'DeploymentRecord'), ], }, 'AI & Automation': { 'models': [ ('system', 'IntegrationSettings'), ('system', 'AIPrompt'), ('system', 'Strategy'), ('system', 'AuthorProfile'), ('system', 'APIKey'), ('system', 'WebhookConfig'), ('automation', 'AutomationConfig'), ('automation', 'AutomationRun'), ], }, 'System Settings': { 'models': [ ('contenttypes', 'ContentType'), ('system', 'ContentTemplate'), ('system', 'TaxonomyConfig'), ('system', 'SystemSetting'), ('system', 'ContentTypeConfig'), ('system', 'NotificationConfig'), ], }, 'Django Admin': { 'models': [ ('auth', 'Group'), ('auth', 'Permission'), ('igny8_core_auth', 'PasswordResetToken'), ('sessions', 'Session'), ], }, 'Tasks & Logging': { 'models': [ ('ai', 'AITaskLog'), ('system', 'AuditLog'), ('admin', 'LogEntry'), ('django_celery_results', 'TaskResult'), ('django_celery_results', 'GroupResult'), ], }, } # ALWAYS build and return our custom organized app list # regardless of app_label parameter (for consistent sidebar on all pages) organized_apps = [] # Add Dashboard link as first item organized_apps.append({ 'name': '📊 Dashboard', 'app_label': '_dashboard', 'app_url': '/admin/dashboard/', 'has_module_perms': True, 'models': [], }) # Add Reports section with links to all reports organized_apps.append({ 'name': 'Reports & Analytics', 'app_label': '_reports', 'app_url': '#', 'has_module_perms': True, 'models': [ { 'name': 'Revenue Report', 'object_name': 'RevenueReport', 'admin_url': '/admin/reports/revenue/', 'view_only': True, 'perms': {'view': True}, }, { 'name': 'Usage Report', 'object_name': 'UsageReport', 'admin_url': '/admin/reports/usage/', 'view_only': True, 'perms': {'view': True}, }, { 'name': 'Content Report', 'object_name': 'ContentReport', 'admin_url': '/admin/reports/content/', 'view_only': True, 'perms': {'view': True}, }, { 'name': 'Data Quality Report', 'object_name': 'DataQualityReport', 'admin_url': '/admin/reports/data-quality/', 'view_only': True, 'perms': {'view': True}, }, { 'name': 'Token Usage Report', 'object_name': 'TokenUsageReport', 'admin_url': '/admin/reports/token-usage/', 'view_only': True, 'perms': {'view': True}, }, { 'name': 'AI Cost Analysis', 'object_name': 'AICostAnalysis', 'admin_url': '/admin/reports/ai-cost-analysis/', 'view_only': True, 'perms': {'view': True}, }, ], }) for group_name, group_config in custom_groups.items(): group_models = [] for app_label, model_name in group_config['models']: # Find the model in app_dict for app in app_dict.values(): if app['app_label'] == app_label: for model in app.get('models', []): if model['object_name'] == model_name: group_models.append(model) break if group_models: # Get the first model's app_label to use as the real app_label first_model_app_label = group_config['models'][0][0] organized_apps.append({ 'name': group_name, 'app_label': first_model_app_label, # Use real app_label, not fake one 'app_url': f'/admin/{first_model_app_label}/', # Real URL, not '#' 'has_module_perms': True, 'models': group_models, }) return organized_apps # Instantiate custom admin site admin_site = Igny8AdminSite(name='admin')