django-cfg 1.2.7__py3-none-any.whl → 1.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/urls.py +2 -2
- django_cfg/modules/django_unfold/callbacks/__init__.py +9 -0
- django_cfg/modules/django_unfold/callbacks/actions.py +50 -0
- django_cfg/modules/django_unfold/callbacks/base.py +98 -0
- django_cfg/modules/django_unfold/callbacks/charts.py +224 -0
- django_cfg/modules/django_unfold/callbacks/commands.py +40 -0
- django_cfg/modules/django_unfold/callbacks/main.py +191 -0
- django_cfg/modules/django_unfold/callbacks/revolution.py +76 -0
- django_cfg/modules/django_unfold/callbacks/statistics.py +240 -0
- django_cfg/modules/django_unfold/callbacks/system.py +180 -0
- django_cfg/modules/django_unfold/callbacks/users.py +65 -0
- django_cfg/modules/django_unfold/models/config.py +10 -3
- django_cfg/modules/django_unfold/tailwind.py +68 -0
- django_cfg/templates/admin/components/action_grid.html +49 -0
- django_cfg/templates/admin/components/card.html +50 -0
- django_cfg/templates/admin/components/data_table.html +67 -0
- django_cfg/templates/admin/components/metric_card.html +39 -0
- django_cfg/templates/admin/components/modal.html +58 -0
- django_cfg/templates/admin/components/progress_bar.html +25 -0
- django_cfg/templates/admin/components/section_header.html +26 -0
- django_cfg/templates/admin/components/stat_item.html +32 -0
- django_cfg/templates/admin/components/stats_grid.html +72 -0
- django_cfg/templates/admin/components/status_badge.html +28 -0
- django_cfg/templates/admin/components/user_avatar.html +27 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +7 -7
- django_cfg/templates/admin/snippets/components/activity_tracker.html +48 -11
- django_cfg/templates/admin/snippets/components/charts_section.html +63 -13
- django_cfg/templates/admin/snippets/components/django_commands.html +18 -18
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -47
- django_cfg/templates/admin/snippets/components/recent_activity.html +28 -38
- django_cfg/templates/admin/snippets/components/recent_users_table.html +22 -53
- django_cfg/templates/admin/snippets/components/stats_cards.html +2 -66
- django_cfg/templates/admin/snippets/components/system_health.html +13 -63
- django_cfg/templates/admin/snippets/components/system_metrics.html +8 -25
- django_cfg/templates/admin/snippets/tabs/commands_tab.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +4 -4
- django_cfg/templates/admin/snippets/zones/zones_table.html +12 -33
- django_cfg/templatetags/django_cfg.py +2 -1
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/METADATA +2 -1
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/RECORD +44 -24
- django_cfg/modules/django_unfold/callbacks.py +0 -795
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,795 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Base Unfold Dashboard Callbacks for Django CFG Toolkit
|
3
|
-
|
4
|
-
Provides comprehensive system monitoring and dashboard functionality.
|
5
|
-
Following CRITICAL_REQUIREMENTS.md - NO raw dicts, ALL type-safe.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import json
|
9
|
-
import logging
|
10
|
-
import os
|
11
|
-
import shutil
|
12
|
-
from typing import Dict, Any, List
|
13
|
-
from datetime import timedelta
|
14
|
-
|
15
|
-
from django.db.models import Count
|
16
|
-
from django.db.models.functions import TruncDate
|
17
|
-
from django.utils import timezone
|
18
|
-
from django.conf import settings
|
19
|
-
from django.urls import get_resolver
|
20
|
-
from django.db import connection
|
21
|
-
from django.core.cache import cache
|
22
|
-
from django.apps import apps
|
23
|
-
|
24
|
-
from ..base import BaseCfgModule
|
25
|
-
from .models.dashboard import DashboardData, StatCard, SystemHealthItem, QuickAction
|
26
|
-
from .icons import Icons
|
27
|
-
|
28
|
-
logger = logging.getLogger(__name__)
|
29
|
-
|
30
|
-
|
31
|
-
def get_available_commands():
|
32
|
-
"""Get all available Django management commands."""
|
33
|
-
from django.core.management import get_commands
|
34
|
-
from django.core.management.base import BaseCommand
|
35
|
-
import importlib
|
36
|
-
|
37
|
-
commands_dict = get_commands()
|
38
|
-
commands_list = []
|
39
|
-
|
40
|
-
for command_name, app_name in commands_dict.items():
|
41
|
-
try:
|
42
|
-
# Try to get command description
|
43
|
-
if app_name == 'django_cfg':
|
44
|
-
module_path = f'django_cfg.management.commands.{command_name}'
|
45
|
-
else:
|
46
|
-
module_path = f'{app_name}.management.commands.{command_name}'
|
47
|
-
|
48
|
-
try:
|
49
|
-
command_module = importlib.import_module(module_path)
|
50
|
-
if hasattr(command_module, 'Command'):
|
51
|
-
command_class = command_module.Command
|
52
|
-
description = getattr(command_class, 'help', f'{command_name} command')
|
53
|
-
else:
|
54
|
-
description = f'{command_name} command'
|
55
|
-
except ImportError:
|
56
|
-
description = f'{command_name} command'
|
57
|
-
|
58
|
-
commands_list.append({
|
59
|
-
'name': command_name,
|
60
|
-
'app': app_name,
|
61
|
-
'description': description,
|
62
|
-
'is_core': app_name.startswith('django.'),
|
63
|
-
'is_custom': app_name == 'django_cfg',
|
64
|
-
})
|
65
|
-
except Exception:
|
66
|
-
# Skip problematic commands
|
67
|
-
continue
|
68
|
-
|
69
|
-
return commands_list
|
70
|
-
|
71
|
-
|
72
|
-
def get_commands_by_category():
|
73
|
-
"""Get commands categorized by type."""
|
74
|
-
commands = get_available_commands()
|
75
|
-
|
76
|
-
categorized = {
|
77
|
-
'django_cfg': [],
|
78
|
-
'django_core': [],
|
79
|
-
'third_party': [],
|
80
|
-
'project': [],
|
81
|
-
}
|
82
|
-
|
83
|
-
for cmd in commands:
|
84
|
-
if cmd['app'] == 'django_cfg':
|
85
|
-
categorized['django_cfg'].append(cmd)
|
86
|
-
elif cmd['app'].startswith('django.'):
|
87
|
-
categorized['django_core'].append(cmd)
|
88
|
-
elif cmd['app'].startswith(('src.', 'api.', 'accounts.')):
|
89
|
-
categorized['project'].append(cmd)
|
90
|
-
else:
|
91
|
-
categorized['third_party'].append(cmd)
|
92
|
-
|
93
|
-
return categorized
|
94
|
-
|
95
|
-
|
96
|
-
def get_user_admin_urls():
|
97
|
-
"""Get admin URLs for user model."""
|
98
|
-
try:
|
99
|
-
from django.contrib.auth import get_user_model
|
100
|
-
User = get_user_model()
|
101
|
-
|
102
|
-
app_label = User._meta.app_label
|
103
|
-
model_name = User._meta.model_name
|
104
|
-
|
105
|
-
return {
|
106
|
-
'changelist': f'admin:{app_label}_{model_name}_changelist',
|
107
|
-
'add': f'admin:{app_label}_{model_name}_add',
|
108
|
-
'change': f'admin:{app_label}_{model_name}_change/{{id}}/',
|
109
|
-
'delete': f'admin:{app_label}_{model_name}_delete/{{id}}/',
|
110
|
-
'view': f'admin:{app_label}_{model_name}_view/{{id}}/',
|
111
|
-
}
|
112
|
-
except Exception:
|
113
|
-
# Universal fallback - return admin index for all actions
|
114
|
-
return {
|
115
|
-
'changelist': 'admin:index',
|
116
|
-
'add': 'admin:index',
|
117
|
-
'change': 'admin:index',
|
118
|
-
'delete': 'admin:index',
|
119
|
-
'view': 'admin:index',
|
120
|
-
}
|
121
|
-
|
122
|
-
|
123
|
-
class UnfoldCallbacks(BaseCfgModule):
|
124
|
-
"""
|
125
|
-
Base Unfold dashboard callbacks with full system monitoring.
|
126
|
-
|
127
|
-
Provides comprehensive dashboard functionality using Pydantic models
|
128
|
-
for type safety and data validation.
|
129
|
-
"""
|
130
|
-
|
131
|
-
def _get_user_model(self):
|
132
|
-
"""Get the user model safely."""
|
133
|
-
from django.contrib.auth import get_user_model
|
134
|
-
return get_user_model()
|
135
|
-
|
136
|
-
def get_user_statistics(self) -> List[StatCard]:
|
137
|
-
"""Get user-related statistics as Pydantic models."""
|
138
|
-
try:
|
139
|
-
User = self._get_user_model()
|
140
|
-
|
141
|
-
total_users = User.objects.count()
|
142
|
-
active_users = User.objects.filter(is_active=True).count()
|
143
|
-
new_users_7d = User.objects.filter(
|
144
|
-
date_joined__gte=timezone.now() - timedelta(days=7)
|
145
|
-
).count()
|
146
|
-
staff_users = User.objects.filter(is_staff=True).count()
|
147
|
-
|
148
|
-
return [
|
149
|
-
StatCard(
|
150
|
-
title="Total Users",
|
151
|
-
value=f"{total_users:,}",
|
152
|
-
icon=Icons.PEOPLE,
|
153
|
-
change=f"+{new_users_7d}" if new_users_7d > 0 else None,
|
154
|
-
change_type="positive" if new_users_7d > 0 else "neutral",
|
155
|
-
description="Registered users",
|
156
|
-
),
|
157
|
-
StatCard(
|
158
|
-
title="Active Users",
|
159
|
-
value=f"{active_users:,}",
|
160
|
-
icon=Icons.PERSON,
|
161
|
-
change=(
|
162
|
-
f"{(active_users/total_users*100):.1f}%"
|
163
|
-
if total_users > 0
|
164
|
-
else "0%"
|
165
|
-
),
|
166
|
-
change_type=(
|
167
|
-
"positive" if active_users > total_users * 0.7 else "neutral"
|
168
|
-
),
|
169
|
-
description="Currently active",
|
170
|
-
),
|
171
|
-
StatCard(
|
172
|
-
title="New This Week",
|
173
|
-
value=f"{new_users_7d:,}",
|
174
|
-
icon=Icons.PERSON_ADD,
|
175
|
-
change_type="positive" if new_users_7d > 0 else "neutral",
|
176
|
-
description="Last 7 days",
|
177
|
-
),
|
178
|
-
StatCard(
|
179
|
-
title="Staff Members",
|
180
|
-
value=f"{staff_users:,}",
|
181
|
-
icon=Icons.ADMIN_PANEL_SETTINGS,
|
182
|
-
change=(
|
183
|
-
f"{(staff_users/total_users*100):.1f}%" if total_users > 0 else "0%"
|
184
|
-
),
|
185
|
-
change_type="neutral",
|
186
|
-
description="Administrative access",
|
187
|
-
),
|
188
|
-
]
|
189
|
-
except Exception as e:
|
190
|
-
logger.error(f"Error getting user statistics: {e}")
|
191
|
-
return [
|
192
|
-
StatCard(
|
193
|
-
title="Users",
|
194
|
-
value="N/A",
|
195
|
-
icon=Icons.PEOPLE,
|
196
|
-
description="Data unavailable",
|
197
|
-
)
|
198
|
-
]
|
199
|
-
|
200
|
-
def get_system_health(self) -> List[SystemHealthItem]:
|
201
|
-
"""Get system health status as Pydantic models."""
|
202
|
-
health_items = []
|
203
|
-
|
204
|
-
# Database health
|
205
|
-
try:
|
206
|
-
with connection.cursor() as cursor:
|
207
|
-
cursor.execute("SELECT 1")
|
208
|
-
health_items.append(
|
209
|
-
SystemHealthItem(
|
210
|
-
component="database",
|
211
|
-
status="healthy",
|
212
|
-
description="Connection successful",
|
213
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
214
|
-
health_percentage=95,
|
215
|
-
)
|
216
|
-
)
|
217
|
-
except Exception as e:
|
218
|
-
health_items.append(
|
219
|
-
SystemHealthItem(
|
220
|
-
component="database",
|
221
|
-
status="error",
|
222
|
-
description=f"Connection failed: {str(e)[:50]}",
|
223
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
224
|
-
health_percentage=0,
|
225
|
-
)
|
226
|
-
)
|
227
|
-
|
228
|
-
# Cache health
|
229
|
-
try:
|
230
|
-
cache.set("health_check", "ok", 10)
|
231
|
-
if cache.get("health_check") == "ok":
|
232
|
-
health_items.append(
|
233
|
-
SystemHealthItem(
|
234
|
-
component="cache",
|
235
|
-
status="healthy",
|
236
|
-
description="Cache operational",
|
237
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
238
|
-
health_percentage=90,
|
239
|
-
)
|
240
|
-
)
|
241
|
-
else:
|
242
|
-
health_items.append(
|
243
|
-
SystemHealthItem(
|
244
|
-
component="cache",
|
245
|
-
status="warning",
|
246
|
-
description="Cache not responding",
|
247
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
248
|
-
health_percentage=50,
|
249
|
-
)
|
250
|
-
)
|
251
|
-
except Exception:
|
252
|
-
health_items.append(
|
253
|
-
SystemHealthItem(
|
254
|
-
component="cache",
|
255
|
-
status="unknown",
|
256
|
-
description="Cache not configured",
|
257
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
258
|
-
health_percentage=0,
|
259
|
-
)
|
260
|
-
)
|
261
|
-
|
262
|
-
# Storage health
|
263
|
-
try:
|
264
|
-
total, used, free = shutil.disk_usage("/")
|
265
|
-
usage_percentage = (used / total) * 100
|
266
|
-
free_percentage = 100 - usage_percentage
|
267
|
-
|
268
|
-
if free_percentage > 20:
|
269
|
-
status = "healthy"
|
270
|
-
desc = f"Disk space: {free_percentage:.1f}% free"
|
271
|
-
elif free_percentage > 10:
|
272
|
-
status = "warning"
|
273
|
-
desc = f"Low disk space: {free_percentage:.1f}% free"
|
274
|
-
else:
|
275
|
-
status = "error"
|
276
|
-
desc = f"Critical disk space: {free_percentage:.1f}% free"
|
277
|
-
|
278
|
-
health_items.append(
|
279
|
-
SystemHealthItem(
|
280
|
-
component="storage",
|
281
|
-
status=status,
|
282
|
-
description=desc,
|
283
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
284
|
-
health_percentage=int(free_percentage),
|
285
|
-
)
|
286
|
-
)
|
287
|
-
except Exception as e:
|
288
|
-
health_items.append(
|
289
|
-
SystemHealthItem(
|
290
|
-
component="storage",
|
291
|
-
status="error",
|
292
|
-
description="Storage check failed",
|
293
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
294
|
-
health_percentage=0,
|
295
|
-
)
|
296
|
-
)
|
297
|
-
|
298
|
-
# API health
|
299
|
-
health_items.append(
|
300
|
-
SystemHealthItem(
|
301
|
-
component="api",
|
302
|
-
status="healthy",
|
303
|
-
description="API server running",
|
304
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
305
|
-
health_percentage=100,
|
306
|
-
)
|
307
|
-
)
|
308
|
-
|
309
|
-
return health_items
|
310
|
-
|
311
|
-
def get_quick_actions(self) -> List[QuickAction]:
|
312
|
-
"""Get quick action buttons as Pydantic models."""
|
313
|
-
# Get user admin URLs dynamically based on AUTH_USER_MODEL
|
314
|
-
user_admin_urls = get_user_admin_urls()
|
315
|
-
|
316
|
-
actions = [
|
317
|
-
QuickAction(
|
318
|
-
title="Add User",
|
319
|
-
description="Create new user account",
|
320
|
-
icon=Icons.PERSON_ADD,
|
321
|
-
link=user_admin_urls["add"],
|
322
|
-
color="primary",
|
323
|
-
category="admin",
|
324
|
-
),
|
325
|
-
QuickAction(
|
326
|
-
title="Support Tickets",
|
327
|
-
description="Manage support tickets",
|
328
|
-
icon=Icons.SUPPORT_AGENT,
|
329
|
-
link="admin:django_cfg_support_ticket_changelist",
|
330
|
-
color="primary",
|
331
|
-
category="support",
|
332
|
-
),
|
333
|
-
QuickAction(
|
334
|
-
title="Health Check",
|
335
|
-
description="System health status",
|
336
|
-
icon=Icons.HEALTH_AND_SAFETY,
|
337
|
-
link="/cfg/health/",
|
338
|
-
color="success",
|
339
|
-
category="system",
|
340
|
-
),
|
341
|
-
]
|
342
|
-
|
343
|
-
# # Automatically add Constance settings if configured
|
344
|
-
# if self._is_constance_configured():
|
345
|
-
# actions.append(
|
346
|
-
# QuickAction(
|
347
|
-
# title="System Settings",
|
348
|
-
# description="Configure dynamic settings",
|
349
|
-
# icon="settings",
|
350
|
-
# link="/admin/constance/config/",
|
351
|
-
# color="warning",
|
352
|
-
# category="admin",
|
353
|
-
# )
|
354
|
-
# )
|
355
|
-
|
356
|
-
return actions
|
357
|
-
|
358
|
-
# def _is_constance_configured(self) -> bool:
|
359
|
-
# """Check if Constance is configured."""
|
360
|
-
# try:
|
361
|
-
# from django.conf import settings
|
362
|
-
# return bool(getattr(settings, 'CONSTANCE_CONFIG', {}))
|
363
|
-
# except Exception:
|
364
|
-
# return False
|
365
|
-
|
366
|
-
def get_support_statistics(self) -> List[StatCard]:
|
367
|
-
"""Get support ticket statistics as Pydantic models."""
|
368
|
-
try:
|
369
|
-
# Check if support is enabled
|
370
|
-
if not self.is_support_enabled():
|
371
|
-
return []
|
372
|
-
|
373
|
-
from django_cfg.apps.support.models import Ticket, Message
|
374
|
-
|
375
|
-
total_tickets = Ticket.objects.count()
|
376
|
-
open_tickets = Ticket.objects.filter(status='open').count()
|
377
|
-
resolved_tickets = Ticket.objects.filter(status='resolved').count()
|
378
|
-
new_tickets_7d = Ticket.objects.filter(
|
379
|
-
created_at__gte=timezone.now() - timedelta(days=7)
|
380
|
-
).count()
|
381
|
-
|
382
|
-
return [
|
383
|
-
StatCard(
|
384
|
-
title="Total Tickets",
|
385
|
-
value=f"{total_tickets:,}",
|
386
|
-
icon=Icons.SUPPORT_AGENT,
|
387
|
-
change=f"+{new_tickets_7d}" if new_tickets_7d > 0 else None,
|
388
|
-
change_type="positive" if new_tickets_7d > 0 else "neutral",
|
389
|
-
description="All support tickets",
|
390
|
-
),
|
391
|
-
StatCard(
|
392
|
-
title="Open Tickets",
|
393
|
-
value=f"{open_tickets:,}",
|
394
|
-
icon=Icons.PENDING,
|
395
|
-
change=(
|
396
|
-
f"{(open_tickets/total_tickets*100):.1f}%"
|
397
|
-
if total_tickets > 0
|
398
|
-
else "0%"
|
399
|
-
),
|
400
|
-
change_type=(
|
401
|
-
"negative" if open_tickets > total_tickets * 0.3
|
402
|
-
else "positive" if open_tickets == 0
|
403
|
-
else "neutral"
|
404
|
-
),
|
405
|
-
description="Awaiting response",
|
406
|
-
),
|
407
|
-
StatCard(
|
408
|
-
title="Resolved",
|
409
|
-
value=f"{resolved_tickets:,}",
|
410
|
-
icon=Icons.CHECK_CIRCLE,
|
411
|
-
change=(
|
412
|
-
f"{(resolved_tickets/total_tickets*100):.1f}%"
|
413
|
-
if total_tickets > 0
|
414
|
-
else "0%"
|
415
|
-
),
|
416
|
-
change_type="positive",
|
417
|
-
description="Successfully resolved",
|
418
|
-
),
|
419
|
-
StatCard(
|
420
|
-
title="New This Week",
|
421
|
-
value=f"{new_tickets_7d:,}",
|
422
|
-
icon=Icons.NEW_RELEASES,
|
423
|
-
change_type="positive" if new_tickets_7d > 0 else "neutral",
|
424
|
-
description="Last 7 days",
|
425
|
-
),
|
426
|
-
]
|
427
|
-
except Exception as e:
|
428
|
-
logger.error(f"Error getting support statistics: {e}")
|
429
|
-
return [
|
430
|
-
StatCard(
|
431
|
-
title="Support",
|
432
|
-
value="N/A",
|
433
|
-
icon=Icons.SUPPORT_AGENT,
|
434
|
-
description="Data unavailable",
|
435
|
-
)
|
436
|
-
]
|
437
|
-
|
438
|
-
def get_django_commands(self) -> Dict[str, Any]:
|
439
|
-
"""Get Django management commands information."""
|
440
|
-
try:
|
441
|
-
commands = get_available_commands()
|
442
|
-
categorized = get_commands_by_category()
|
443
|
-
|
444
|
-
return {
|
445
|
-
"commands": commands,
|
446
|
-
"categorized": categorized,
|
447
|
-
"total_commands": len(commands),
|
448
|
-
"categories": list(categorized.keys()),
|
449
|
-
"core_commands": len([cmd for cmd in commands if cmd['is_core']]),
|
450
|
-
"custom_commands": len([cmd for cmd in commands if cmd['is_custom']]),
|
451
|
-
}
|
452
|
-
except Exception as e:
|
453
|
-
logger.error(f"Error getting Django commands: {e}")
|
454
|
-
# Return safe fallback to prevent dashboard from breaking
|
455
|
-
return {
|
456
|
-
"commands": [],
|
457
|
-
"categorized": {},
|
458
|
-
"total_commands": 0,
|
459
|
-
"categories": [],
|
460
|
-
"core_commands": 0,
|
461
|
-
"custom_commands": 0,
|
462
|
-
}
|
463
|
-
|
464
|
-
def get_revolution_zones_data(self) -> tuple[list, dict]:
|
465
|
-
"""Get Django Revolution zones data."""
|
466
|
-
try:
|
467
|
-
# Try to get revolution config from Django settings
|
468
|
-
revolution_config = getattr(settings, "DJANGO_REVOLUTION", {})
|
469
|
-
zones = revolution_config.get("zones", {})
|
470
|
-
api_prefix = revolution_config.get("api_prefix", "apix")
|
471
|
-
|
472
|
-
zones_data = []
|
473
|
-
total_apps = 0
|
474
|
-
total_endpoints = 0
|
475
|
-
|
476
|
-
for zone_name, zone_config in zones.items():
|
477
|
-
# Handle both dict and object access
|
478
|
-
if isinstance(zone_config, dict):
|
479
|
-
title = zone_config.get("title", zone_name.title())
|
480
|
-
description = zone_config.get("description", f"{zone_name} zone")
|
481
|
-
apps = zone_config.get("apps", [])
|
482
|
-
public = zone_config.get("public", False)
|
483
|
-
auth_required = zone_config.get("auth_required", True)
|
484
|
-
else:
|
485
|
-
# Handle object access (for ZoneConfig instances)
|
486
|
-
title = getattr(zone_config, "title", zone_name.title())
|
487
|
-
description = getattr(zone_config, "description", f"{zone_name} zone")
|
488
|
-
apps = getattr(zone_config, "apps", [])
|
489
|
-
public = getattr(zone_config, "public", False)
|
490
|
-
auth_required = getattr(zone_config, "auth_required", True)
|
491
|
-
|
492
|
-
# Count actual endpoints by checking URL patterns (simplified estimate)
|
493
|
-
endpoint_count = len(apps) * 3 # Conservative estimate
|
494
|
-
|
495
|
-
zones_data.append({
|
496
|
-
"name": zone_name,
|
497
|
-
"title": title,
|
498
|
-
"description": description,
|
499
|
-
"app_count": len(apps),
|
500
|
-
"endpoint_count": endpoint_count,
|
501
|
-
"status": "active",
|
502
|
-
"public": public,
|
503
|
-
"auth_required": auth_required,
|
504
|
-
"schema_url": f"/schema/{zone_name}/schema/",
|
505
|
-
"swagger_url": f"/schema/{zone_name}/schema/swagger/",
|
506
|
-
"redoc_url": f"/schema/{zone_name}/redoc/",
|
507
|
-
"api_url": f"/{api_prefix}/{zone_name}/",
|
508
|
-
})
|
509
|
-
|
510
|
-
total_apps += len(apps)
|
511
|
-
total_endpoints += endpoint_count
|
512
|
-
|
513
|
-
return zones_data, {
|
514
|
-
"total_apps": total_apps,
|
515
|
-
"total_endpoints": total_endpoints,
|
516
|
-
"total_zones": len(zones),
|
517
|
-
}
|
518
|
-
except Exception as e:
|
519
|
-
logger.error(f"Error getting revolution zones: {e}")
|
520
|
-
return [], {
|
521
|
-
"total_apps": 0,
|
522
|
-
"total_endpoints": 0,
|
523
|
-
"total_zones": 0,
|
524
|
-
}
|
525
|
-
|
526
|
-
def get_recent_users(self) -> List[Dict[str, Any]]:
|
527
|
-
"""Get recent users data for template."""
|
528
|
-
try:
|
529
|
-
User = self._get_user_model()
|
530
|
-
recent_users = User.objects.select_related().order_by("-date_joined")[:10]
|
531
|
-
|
532
|
-
# Get admin URLs for user model
|
533
|
-
user_admin_urls = get_user_admin_urls()
|
534
|
-
|
535
|
-
return [
|
536
|
-
{
|
537
|
-
"id": user.id,
|
538
|
-
"username": user.username,
|
539
|
-
"email": user.email or "No email",
|
540
|
-
"date_joined": (
|
541
|
-
user.date_joined.strftime("%Y-%m-%d")
|
542
|
-
if user.date_joined
|
543
|
-
else "Unknown"
|
544
|
-
),
|
545
|
-
"is_active": user.is_active,
|
546
|
-
"is_staff": user.is_staff,
|
547
|
-
"is_superuser": user.is_superuser,
|
548
|
-
"last_login": user.last_login,
|
549
|
-
"admin_urls": {
|
550
|
-
"change": (
|
551
|
-
user_admin_urls["change"].format(id=user.id)
|
552
|
-
if user.id
|
553
|
-
else None
|
554
|
-
),
|
555
|
-
"view": (
|
556
|
-
user_admin_urls["view"].format(id=user.id) if user.id else None
|
557
|
-
),
|
558
|
-
},
|
559
|
-
}
|
560
|
-
for user in recent_users
|
561
|
-
]
|
562
|
-
except Exception as e:
|
563
|
-
logger.error(f"Error getting recent users: {e}")
|
564
|
-
return []
|
565
|
-
|
566
|
-
def get_app_statistics(self) -> Dict[str, Any]:
|
567
|
-
"""Get statistics for all apps and their models."""
|
568
|
-
stats = {"apps": {}, "total_records": 0, "total_models": 0, "total_apps": 0}
|
569
|
-
|
570
|
-
# Get all installed apps
|
571
|
-
for app_config in apps.get_app_configs():
|
572
|
-
app_label = app_config.label
|
573
|
-
|
574
|
-
# Skip system apps
|
575
|
-
if app_label in ["admin", "contenttypes", "sessions", "auth"]:
|
576
|
-
continue
|
577
|
-
|
578
|
-
app_stats = self._get_app_stats(app_label)
|
579
|
-
if app_stats:
|
580
|
-
stats["apps"][app_label] = app_stats
|
581
|
-
stats["total_records"] += app_stats.get("total_records", 0)
|
582
|
-
stats["total_models"] += app_stats.get("model_count", 0)
|
583
|
-
stats["total_apps"] += 1
|
584
|
-
|
585
|
-
return stats
|
586
|
-
|
587
|
-
def _get_app_stats(self, app_label: str) -> Dict[str, Any]:
|
588
|
-
"""Get statistics for a specific app."""
|
589
|
-
try:
|
590
|
-
app_config = apps.get_app_config(app_label)
|
591
|
-
# Convert generator to list to avoid len() error
|
592
|
-
models_list = list(app_config.get_models())
|
593
|
-
|
594
|
-
if not models_list:
|
595
|
-
return None
|
596
|
-
|
597
|
-
app_stats = {
|
598
|
-
"name": app_config.verbose_name or app_label.title(),
|
599
|
-
"models": {},
|
600
|
-
"total_records": 0,
|
601
|
-
"model_count": len(models_list),
|
602
|
-
}
|
603
|
-
|
604
|
-
for model in models_list:
|
605
|
-
try:
|
606
|
-
# Get model statistics
|
607
|
-
model_stats = self._get_model_stats(model)
|
608
|
-
if model_stats:
|
609
|
-
app_stats["models"][model._meta.model_name] = model_stats
|
610
|
-
app_stats["total_records"] += model_stats.get("count", 0)
|
611
|
-
except Exception:
|
612
|
-
continue
|
613
|
-
|
614
|
-
return app_stats
|
615
|
-
|
616
|
-
except Exception:
|
617
|
-
return None
|
618
|
-
|
619
|
-
def _get_model_stats(self, model) -> Dict[str, Any]:
|
620
|
-
"""Get statistics for a specific model."""
|
621
|
-
try:
|
622
|
-
# Get basic model info
|
623
|
-
model_stats = {
|
624
|
-
"name": model._meta.verbose_name_plural
|
625
|
-
or model._meta.verbose_name
|
626
|
-
or model._meta.model_name,
|
627
|
-
"count": model.objects.count(),
|
628
|
-
"fields_count": len(model._meta.fields),
|
629
|
-
"admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
|
630
|
-
}
|
631
|
-
|
632
|
-
return model_stats
|
633
|
-
|
634
|
-
except Exception:
|
635
|
-
return None
|
636
|
-
|
637
|
-
def get_system_metrics(self) -> Dict[str, Any]:
|
638
|
-
"""Get system metrics for dashboard."""
|
639
|
-
metrics = {}
|
640
|
-
|
641
|
-
# Database metrics
|
642
|
-
try:
|
643
|
-
with connection.cursor() as cursor:
|
644
|
-
cursor.execute("SELECT 1")
|
645
|
-
metrics["database"] = {
|
646
|
-
"status": "healthy",
|
647
|
-
"type": "PostgreSQL",
|
648
|
-
"health_percentage": 95,
|
649
|
-
"description": "Connection successful",
|
650
|
-
}
|
651
|
-
except Exception as e:
|
652
|
-
metrics["database"] = {
|
653
|
-
"status": "error",
|
654
|
-
"type": "PostgreSQL",
|
655
|
-
"health_percentage": 0,
|
656
|
-
"description": f"Connection failed: {str(e)}",
|
657
|
-
}
|
658
|
-
|
659
|
-
# Cache metrics
|
660
|
-
try:
|
661
|
-
cache.set("health_check", "ok", 10)
|
662
|
-
cache_result = cache.get("health_check")
|
663
|
-
if cache_result == "ok":
|
664
|
-
metrics["cache"] = {
|
665
|
-
"status": "healthy",
|
666
|
-
"type": "Memory Cache",
|
667
|
-
"health_percentage": 90,
|
668
|
-
"description": "Cache working properly",
|
669
|
-
}
|
670
|
-
else:
|
671
|
-
metrics["cache"] = {
|
672
|
-
"status": "warning",
|
673
|
-
"type": "Memory Cache",
|
674
|
-
"health_percentage": 50,
|
675
|
-
"description": "Cache response delayed",
|
676
|
-
}
|
677
|
-
except Exception as e:
|
678
|
-
metrics["cache"] = {
|
679
|
-
"status": "error",
|
680
|
-
"type": "Memory Cache",
|
681
|
-
"health_percentage": 0,
|
682
|
-
"description": f"Cache error: {str(e)}",
|
683
|
-
}
|
684
|
-
|
685
|
-
return metrics
|
686
|
-
|
687
|
-
def main_dashboard_callback(self, request, context: Dict[str, Any]) -> Dict[str, Any]:
|
688
|
-
"""
|
689
|
-
Main dashboard callback function with comprehensive system data.
|
690
|
-
|
691
|
-
Returns all dashboard data as Pydantic models for type safety.
|
692
|
-
"""
|
693
|
-
try:
|
694
|
-
# Get dashboard data using Pydantic models
|
695
|
-
user_stats = self.get_user_statistics()
|
696
|
-
support_stats = self.get_support_statistics()
|
697
|
-
system_health = self.get_system_health()
|
698
|
-
quick_actions = self.get_quick_actions()
|
699
|
-
|
700
|
-
# Combine all stat cards
|
701
|
-
all_stats = user_stats + support_stats
|
702
|
-
|
703
|
-
dashboard_data = DashboardData(
|
704
|
-
stat_cards=all_stats,
|
705
|
-
system_health=system_health,
|
706
|
-
quick_actions=quick_actions,
|
707
|
-
last_updated=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
708
|
-
environment=getattr(settings, "ENVIRONMENT", "development"),
|
709
|
-
)
|
710
|
-
|
711
|
-
# Convert to template context (using to_dict for Unfold compatibility)
|
712
|
-
cards_data = [card.to_dict() for card in dashboard_data.stat_cards]
|
713
|
-
|
714
|
-
context.update({
|
715
|
-
# Statistics cards
|
716
|
-
"cards": cards_data,
|
717
|
-
"user_stats": [card.to_dict() for card in user_stats],
|
718
|
-
"support_stats": [card.to_dict() for card in support_stats],
|
719
|
-
# System health (convert to dict for template)
|
720
|
-
"system_health": {
|
721
|
-
item.component + "_status": item.status
|
722
|
-
for item in dashboard_data.system_health
|
723
|
-
},
|
724
|
-
# System metrics
|
725
|
-
"system_metrics": self.get_system_metrics(),
|
726
|
-
# Quick actions
|
727
|
-
"quick_actions": [
|
728
|
-
action.model_dump() for action in dashboard_data.quick_actions
|
729
|
-
],
|
730
|
-
# Additional categorized actions
|
731
|
-
"admin_actions": [
|
732
|
-
action.model_dump()
|
733
|
-
for action in dashboard_data.quick_actions
|
734
|
-
if action.category == "admin"
|
735
|
-
],
|
736
|
-
"support_actions": [
|
737
|
-
action.model_dump()
|
738
|
-
for action in dashboard_data.quick_actions
|
739
|
-
if action.category == "support"
|
740
|
-
],
|
741
|
-
"system_actions": [
|
742
|
-
action.model_dump()
|
743
|
-
for action in dashboard_data.quick_actions
|
744
|
-
if action.category == "system"
|
745
|
-
],
|
746
|
-
# Revolution zones
|
747
|
-
"zones_table": {
|
748
|
-
"headers": [
|
749
|
-
{"label": "Zone"},
|
750
|
-
{"label": "Title"},
|
751
|
-
{"label": "Apps"},
|
752
|
-
{"label": "Endpoints"},
|
753
|
-
{"label": "Status"},
|
754
|
-
{"label": "Actions"},
|
755
|
-
],
|
756
|
-
"rows": self.get_revolution_zones_data()[0],
|
757
|
-
},
|
758
|
-
# Recent users
|
759
|
-
"recent_users": self.get_recent_users(),
|
760
|
-
"user_admin_urls": get_user_admin_urls(),
|
761
|
-
# App statistics
|
762
|
-
"app_statistics": self.get_app_statistics(),
|
763
|
-
# Django commands
|
764
|
-
"django_commands": self.get_django_commands(),
|
765
|
-
# Meta information
|
766
|
-
"last_updated": dashboard_data.last_updated,
|
767
|
-
"environment": dashboard_data.environment,
|
768
|
-
"dashboard_title": "Django CFG Dashboard",
|
769
|
-
})
|
770
|
-
|
771
|
-
# logger.info(f"Final context keys: {list(context.keys())}")
|
772
|
-
# logger.info(f"Cards in context: {len(context.get('cards', []))}")
|
773
|
-
# logger.info("=== DJANGO_CFG DASHBOARD CALLBACK COMPLETED ===")
|
774
|
-
|
775
|
-
return context
|
776
|
-
|
777
|
-
except Exception as e:
|
778
|
-
logger.error(f"Dashboard callback error: {e}")
|
779
|
-
# Return minimal safe defaults
|
780
|
-
context.update({
|
781
|
-
"cards": [
|
782
|
-
{
|
783
|
-
"title": "System Error",
|
784
|
-
"value": "N/A",
|
785
|
-
"icon": "error",
|
786
|
-
"color": "danger",
|
787
|
-
"description": "Dashboard data unavailable"
|
788
|
-
}
|
789
|
-
],
|
790
|
-
"system_health": {},
|
791
|
-
"quick_actions": [],
|
792
|
-
"last_updated": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
793
|
-
"error": f"Dashboard error: {str(e)}",
|
794
|
-
})
|
795
|
-
return context
|