django-cfg 1.4.88__py3-none-any.whl → 1.4.89__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/centrifugo/views/__init__.py +0 -2
- django_cfg/apps/dashboard/services/__init__.py +2 -0
- django_cfg/apps/dashboard/services/overview_service.py +205 -0
- django_cfg/apps/frontend/test_routing.py +134 -0
- django_cfg/apps/frontend/views.py +69 -28
- django_cfg/apps/urls.py +0 -1
- django_cfg/core/builders/apps_builder.py +0 -58
- django_cfg/modules/django_unfold/__init__.py +5 -24
- django_cfg/modules/django_unfold/models/__init__.py +0 -23
- django_cfg/modules/django_unfold/models/config.py +11 -65
- django_cfg/modules/django_unfold/{dashboard.py → navigation.py} +21 -152
- django_cfg/modules/django_unfold/tailwind.py +2 -4
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/third_party.py +0 -9
- django_cfg/routing/callbacks.py +1 -43
- django_cfg/static/frontend/admin/404/index.html +1 -1
- django_cfg/static/frontend/admin/404.html +1 -1
- django_cfg/static/frontend/admin/500/index.html +1 -1
- django_cfg/static/frontend/admin/_next/static/{D_d9HRw5Yn7BRHAX5q89_ → 0sN9ktsgXH48ygtGSrhfu}/_buildManifest.js +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-9443faa6df24aebf.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{_app-1c0fff0f59a6d683.js → _app-c7dcd3aa616fab68.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{centrifugo-44a8313fa040e9ad.js → centrifugo-f9ecbc3ae0052a03.js} +1 -1
- django_cfg/static/frontend/admin/auth/index.html +1 -1
- django_cfg/static/frontend/admin/index.html +1 -1
- django_cfg/static/frontend/admin/legal/cookies/index.html +1 -1
- django_cfg/static/frontend/admin/legal/privacy/index.html +1 -1
- django_cfg/static/frontend/admin/legal/security/index.html +1 -1
- django_cfg/static/frontend/admin/legal/terms/index.html +1 -1
- django_cfg/static/frontend/admin/private/centrifugo/index.html +1 -1
- django_cfg/static/frontend/admin/private/index.html +1 -1
- django_cfg/static/frontend/admin/private/profile/index.html +1 -1
- django_cfg/static/frontend/admin/private/ui/index.html +2 -2
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/METADATA +1 -1
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/RECORD +39 -143
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +0 -260
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +0 -313
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +0 -803
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +0 -341
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +0 -432
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +0 -33
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +0 -210
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +0 -46
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +0 -123
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +0 -45
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +0 -84
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/stat_cards.html +0 -53
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +0 -91
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/tab_navigation.html +0 -29
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +0 -415
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +0 -61
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +0 -58
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +0 -48
- django_cfg/apps/centrifugo/templatetags/__init__.py +0 -1
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +0 -81
- django_cfg/apps/centrifugo/urls_admin.py +0 -20
- django_cfg/apps/centrifugo/views/dashboard.py +0 -28
- django_cfg/modules/django_dashboard/__init__.py +0 -23
- django_cfg/modules/django_dashboard/components.py +0 -312
- django_cfg/modules/django_dashboard/debug.py +0 -174
- django_cfg/modules/django_dashboard/management/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/debug_dashboard.py +0 -109
- django_cfg/modules/django_dashboard/sections/__init__.py +0 -1
- django_cfg/modules/django_dashboard/sections/base.py +0 -129
- django_cfg/modules/django_dashboard/sections/commands.py +0 -33
- django_cfg/modules/django_dashboard/sections/documentation.py +0 -393
- django_cfg/modules/django_dashboard/sections/overview.py +0 -398
- django_cfg/modules/django_dashboard/sections/stats.py +0 -48
- django_cfg/modules/django_dashboard/sections/system.py +0 -74
- django_cfg/modules/django_dashboard/sections/widgets.py +0 -222
- django_cfg/modules/django_unfold/callbacks/__init__.py +0 -9
- django_cfg/modules/django_unfold/callbacks/actions.py +0 -51
- django_cfg/modules/django_unfold/callbacks/apizones.py +0 -122
- django_cfg/modules/django_unfold/callbacks/base.py +0 -290
- django_cfg/modules/django_unfold/callbacks/charts.py +0 -223
- django_cfg/modules/django_unfold/callbacks/commands.py +0 -40
- django_cfg/modules/django_unfold/callbacks/main.py +0 -322
- django_cfg/modules/django_unfold/callbacks/statistics.py +0 -240
- django_cfg/modules/django_unfold/callbacks/system.py +0 -180
- django_cfg/modules/django_unfold/callbacks/users.py +0 -65
- django_cfg/modules/django_unfold/models/dashboard.py +0 -207
- django_cfg/modules/django_unfold/models/tabs.py +0 -26
- django_cfg/modules/django_unfold/models.py +0 -98
- django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +0 -102
- django_cfg/static/frontend/admin/_next/static/chunks/50314-5ec79b293c2283dd.js +0 -1
- django_cfg/templates/admin/sections/commands_section.html +0 -5
- django_cfg/templates/admin/sections/documentation_section.html +0 -5
- django_cfg/templates/admin/sections/overview_section.html +0 -5
- django_cfg/templates/admin/sections/stats_section.html +0 -5
- django_cfg/templates/admin/sections/system_section.html +0 -5
- django_cfg/templates/admin/sections/widgets_section.html +0 -11
- django_cfg/templates/admin_old/components/action_grid.html +0 -49
- django_cfg/templates/admin_old/components/card.html +0 -50
- django_cfg/templates/admin_old/components/data_table.html +0 -67
- django_cfg/templates/admin_old/components/metric_card.html +0 -39
- django_cfg/templates/admin_old/components/modal.html +0 -58
- django_cfg/templates/admin_old/components/progress_bar.html +0 -20
- django_cfg/templates/admin_old/components/section_header.html +0 -26
- django_cfg/templates/admin_old/components/stat_item.html +0 -32
- django_cfg/templates/admin_old/components/stats_grid.html +0 -72
- django_cfg/templates/admin_old/components/status_badge.html +0 -28
- django_cfg/templates/admin_old/components/user_avatar.html +0 -27
- django_cfg/templates/admin_old/constance/change_list.html +0 -74
- django_cfg/templates/admin_old/constance/includes/default_value.html +0 -24
- django_cfg/templates/admin_old/constance/includes/fieldset_header.html +0 -15
- django_cfg/templates/admin_old/constance/includes/results_list.html +0 -16
- django_cfg/templates/admin_old/constance/includes/setting_row.html +0 -50
- django_cfg/templates/admin_old/constance/includes/table_headers.html +0 -10
- django_cfg/templates/admin_old/examples/component_class_example.html +0 -156
- django_cfg/templates/admin_old/import_export/change_list_export.html +0 -24
- django_cfg/templates/admin_old/import_export/change_list_import.html +0 -24
- django_cfg/templates/admin_old/import_export/change_list_import_export.html +0 -34
- django_cfg/templates/admin_old/index.html +0 -80
- django_cfg/templates/admin_old/index_new.html +0 -119
- django_cfg/templates/admin_old/layouts/base_dashboard.html +0 -62
- django_cfg/templates/admin_old/layouts/dashboard_with_tabs.html +0 -176
- django_cfg/templates/admin_old/sections/commands_section.html +0 -549
- django_cfg/templates/admin_old/sections/documentation_section.html +0 -152
- django_cfg/templates/admin_old/sections/overview_section.html +0 -112
- django_cfg/templates/admin_old/sections/stats_section.html +0 -35
- django_cfg/templates/admin_old/sections/system_section.html +0 -99
- django_cfg/templates/admin_old/sections/widgets_section.html +0 -129
- django_cfg/templates/admin_old/snippets/components/activity_tracker.html +0 -70
- django_cfg/templates/admin_old/snippets/components/charts_section.html +0 -113
- django_cfg/templates/admin_old/snippets/components/django_commands.html +0 -270
- django_cfg/templates/admin_old/snippets/components/quick_actions.html +0 -66
- django_cfg/templates/admin_old/snippets/components/recent_activity_improved.html +0 -25
- django_cfg/templates/admin_old/snippets/components/recent_users_table.html +0 -102
- django_cfg/templates/admin_old/snippets/components/stats_cards.html +0 -4
- django_cfg/templates/admin_old/snippets/components/stats_tiles.html +0 -92
- django_cfg/templates/admin_old/snippets/components/system_health.html +0 -22
- django_cfg/templates/admin_old/snippets/components/system_metrics.html +0 -199
- django_cfg/templates/admin_old/snippets/components/user_permissions.html +0 -57
- django_cfg/templates/admin_old/snippets/tabs/app_stats_tab.html +0 -201
- django_cfg/templates/admin_old/snippets/tabs/commands_tab.html +0 -114
- django_cfg/templates/admin_old/snippets/tabs/documentation_tab.html +0 -42
- django_cfg/templates/admin_old/snippets/tabs/overview_tab.html +0 -116
- django_cfg/templates/admin_old/snippets/tabs/stats_tab.html +0 -89
- django_cfg/templates/admin_old/snippets/tabs/users_tab.html +0 -51
- django_cfg/templates/admin_old/snippets/tabs/widgets_tab.html +0 -38
- django_cfg/templates/admin_old/snippets/zones/zones_table.html +0 -176
- /django_cfg/static/frontend/admin/_next/static/{D_d9HRw5Yn7BRHAX5q89_ → 0sN9ktsgXH48ygtGSrhfu}/_ssgManifest.js +0 -0
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
"""Widgets section for dashboard.
|
|
2
|
-
|
|
3
|
-
Automatically renders widgets from DashboardManager.get_widgets_config()
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from datetime import timedelta
|
|
7
|
-
from typing import Any, Dict, List
|
|
8
|
-
|
|
9
|
-
import psutil
|
|
10
|
-
from django.db.models import Avg
|
|
11
|
-
from django.template import Context, Template
|
|
12
|
-
from django.utils import timezone
|
|
13
|
-
|
|
14
|
-
from .base import DataSection
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class WidgetsSection(DataSection):
|
|
18
|
-
"""
|
|
19
|
-
Widgets section showing automatically generated dashboard widgets.
|
|
20
|
-
|
|
21
|
-
Widgets are defined in DashboardManager.get_widgets_config() and
|
|
22
|
-
can include:
|
|
23
|
-
- System metrics (CPU, Memory, Disk)
|
|
24
|
-
- RPC monitoring stats
|
|
25
|
-
- Custom application widgets
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
template_name = "admin/sections/widgets_section.html"
|
|
29
|
-
title = "Dashboard Widgets"
|
|
30
|
-
icon = "widgets"
|
|
31
|
-
|
|
32
|
-
def get_data(self) -> Dict[str, Any]:
|
|
33
|
-
"""Get widgets configuration from DashboardManager."""
|
|
34
|
-
from django_cfg.modules.django_unfold.dashboard import get_dashboard_manager
|
|
35
|
-
|
|
36
|
-
dashboard_manager = get_dashboard_manager()
|
|
37
|
-
|
|
38
|
-
# Get widgets from dashboard manager (base system widgets)
|
|
39
|
-
widgets = dashboard_manager.get_widgets_config()
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
'widgets': widgets,
|
|
43
|
-
'widgets_count': len(widgets),
|
|
44
|
-
'has_widgets': len(widgets) > 0,
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
def merge_custom_widgets(self, widgets: List[Dict], custom_widgets: List[Any]) -> List[Dict]:
|
|
48
|
-
"""
|
|
49
|
-
Merge custom widgets from dashboard_callback.
|
|
50
|
-
|
|
51
|
-
Allows projects to add widgets via dashboard_callback:
|
|
52
|
-
context["custom_widgets"] = [
|
|
53
|
-
StatsCardsWidget(...),
|
|
54
|
-
...
|
|
55
|
-
]
|
|
56
|
-
"""
|
|
57
|
-
if not custom_widgets:
|
|
58
|
-
return widgets
|
|
59
|
-
|
|
60
|
-
# Convert custom widgets to dicts if they are Pydantic models
|
|
61
|
-
for widget in custom_widgets:
|
|
62
|
-
if hasattr(widget, 'to_dict'):
|
|
63
|
-
widgets.append(widget.to_dict())
|
|
64
|
-
elif hasattr(widget, 'model_dump'):
|
|
65
|
-
widgets.append(widget.model_dump())
|
|
66
|
-
elif isinstance(widget, dict):
|
|
67
|
-
widgets.append(widget)
|
|
68
|
-
|
|
69
|
-
return widgets
|
|
70
|
-
|
|
71
|
-
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
|
72
|
-
"""Add additional context for widget rendering."""
|
|
73
|
-
context = super().get_context_data(**kwargs)
|
|
74
|
-
|
|
75
|
-
# Get base widgets from DashboardManager
|
|
76
|
-
widgets = context['data']['widgets']
|
|
77
|
-
|
|
78
|
-
# Merge custom widgets from dashboard_callback if provided
|
|
79
|
-
custom_widgets_from_callback = kwargs.get('custom_widgets', [])
|
|
80
|
-
if custom_widgets_from_callback:
|
|
81
|
-
widgets = self.merge_custom_widgets(widgets, custom_widgets_from_callback)
|
|
82
|
-
# Update count
|
|
83
|
-
context['data']['widgets_count'] = len(widgets)
|
|
84
|
-
context['data']['has_widgets'] = len(widgets) > 0
|
|
85
|
-
|
|
86
|
-
# Get metrics data first
|
|
87
|
-
metrics_data = {}
|
|
88
|
-
metrics_data.update(self.get_system_metrics())
|
|
89
|
-
|
|
90
|
-
# Add Centrifugo metrics if enabled
|
|
91
|
-
dashboard_manager = self._get_dashboard_manager()
|
|
92
|
-
if dashboard_manager.is_centrifugo_enabled():
|
|
93
|
-
metrics_data.update(self.get_centrifugo_metrics())
|
|
94
|
-
|
|
95
|
-
# Also merge any custom metrics from kwargs
|
|
96
|
-
custom_metrics = kwargs.get('custom_metrics', {})
|
|
97
|
-
if custom_metrics:
|
|
98
|
-
metrics_data.update(custom_metrics)
|
|
99
|
-
|
|
100
|
-
# Process widgets and resolve template variables
|
|
101
|
-
processed_stats_widgets = []
|
|
102
|
-
for widget in widgets:
|
|
103
|
-
if widget.get('type') == 'stats_cards':
|
|
104
|
-
processed_widget = self._process_stats_widget(widget, metrics_data)
|
|
105
|
-
processed_stats_widgets.append(processed_widget)
|
|
106
|
-
|
|
107
|
-
chart_widgets = [w for w in widgets if w.get('type') == 'chart']
|
|
108
|
-
custom_widgets = [w for w in widgets if w.get('type') not in ['stats_cards', 'chart']]
|
|
109
|
-
|
|
110
|
-
# Add processed widgets
|
|
111
|
-
context.update({
|
|
112
|
-
'stats_widgets': processed_stats_widgets,
|
|
113
|
-
'chart_widgets': chart_widgets,
|
|
114
|
-
'custom_widgets': custom_widgets,
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
# Also add metrics for direct access
|
|
118
|
-
context.update(metrics_data)
|
|
119
|
-
|
|
120
|
-
return context
|
|
121
|
-
|
|
122
|
-
def _get_dashboard_manager(self):
|
|
123
|
-
"""Get dashboard manager instance (lazy import to avoid circular dependencies)."""
|
|
124
|
-
from django_cfg.modules.django_unfold.dashboard import get_dashboard_manager
|
|
125
|
-
return get_dashboard_manager()
|
|
126
|
-
|
|
127
|
-
def _process_stats_widget(self, widget: Dict[str, Any], context_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
128
|
-
"""Process StatsCardsWidget and resolve template variables in cards."""
|
|
129
|
-
processed_widget = widget.copy()
|
|
130
|
-
processed_cards = []
|
|
131
|
-
|
|
132
|
-
for card in widget.get('cards', []):
|
|
133
|
-
processed_card = card.copy()
|
|
134
|
-
|
|
135
|
-
# Resolve value_template using Django template engine
|
|
136
|
-
value_template = card.get('value_template', '')
|
|
137
|
-
if '{{' in value_template:
|
|
138
|
-
try:
|
|
139
|
-
template = Template(value_template)
|
|
140
|
-
context = Context(context_data)
|
|
141
|
-
resolved_value = template.render(context)
|
|
142
|
-
processed_card['value_template'] = resolved_value
|
|
143
|
-
except Exception:
|
|
144
|
-
# Keep original if rendering fails
|
|
145
|
-
pass
|
|
146
|
-
|
|
147
|
-
# Also resolve change field if it has template variables
|
|
148
|
-
change_template = card.get('change', '')
|
|
149
|
-
if change_template and '{{' in change_template:
|
|
150
|
-
try:
|
|
151
|
-
template = Template(change_template)
|
|
152
|
-
context = Context(context_data)
|
|
153
|
-
resolved_change = template.render(context)
|
|
154
|
-
processed_card['change'] = resolved_change
|
|
155
|
-
except Exception:
|
|
156
|
-
# Keep original if rendering fails
|
|
157
|
-
pass
|
|
158
|
-
|
|
159
|
-
processed_cards.append(processed_card)
|
|
160
|
-
|
|
161
|
-
processed_widget['cards'] = processed_cards
|
|
162
|
-
return processed_widget
|
|
163
|
-
|
|
164
|
-
def get_system_metrics(self) -> Dict[str, Any]:
|
|
165
|
-
"""Get system metrics for widgets."""
|
|
166
|
-
return {
|
|
167
|
-
'cpu_percent': round(psutil.cpu_percent(interval=0.1), 1),
|
|
168
|
-
'memory_percent': round(psutil.virtual_memory().percent, 1),
|
|
169
|
-
'disk_percent': round(psutil.disk_usage('/').percent, 1),
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
def get_centrifugo_metrics(self) -> Dict[str, Any]:
|
|
173
|
-
"""Get Centrifugo metrics for widgets."""
|
|
174
|
-
try:
|
|
175
|
-
from django_cfg.apps.centrifugo.models import CentrifugoLog
|
|
176
|
-
|
|
177
|
-
# Get stats for last 24 hours
|
|
178
|
-
since = timezone.now() - timedelta(hours=24)
|
|
179
|
-
|
|
180
|
-
logs = CentrifugoLog.objects.filter(created_at__gte=since)
|
|
181
|
-
|
|
182
|
-
total_publishes = logs.count()
|
|
183
|
-
successful_publishes = logs.filter(status='success').count()
|
|
184
|
-
failed_publishes = logs.filter(status='failed').count()
|
|
185
|
-
timeout_publishes = logs.filter(status='timeout').count()
|
|
186
|
-
|
|
187
|
-
success_rate = round((successful_publishes / total_publishes * 100) if total_publishes > 0 else 0, 1)
|
|
188
|
-
|
|
189
|
-
avg_duration = logs.filter(
|
|
190
|
-
duration_ms__isnull=False
|
|
191
|
-
).aggregate(
|
|
192
|
-
avg=Avg('duration_ms')
|
|
193
|
-
)['avg']
|
|
194
|
-
|
|
195
|
-
avg_duration = round(avg_duration, 1) if avg_duration else 0 # Already in ms
|
|
196
|
-
|
|
197
|
-
avg_acks = logs.filter(
|
|
198
|
-
acks_received__isnull=False
|
|
199
|
-
).aggregate(
|
|
200
|
-
avg=Avg('acks_received')
|
|
201
|
-
)['avg']
|
|
202
|
-
|
|
203
|
-
avg_acks = round(avg_acks, 1) if avg_acks else 0
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
'centrifugo_total_publishes': total_publishes,
|
|
207
|
-
'centrifugo_success_rate': success_rate,
|
|
208
|
-
'centrifugo_avg_duration': avg_duration,
|
|
209
|
-
'centrifugo_failed_publishes': failed_publishes,
|
|
210
|
-
'centrifugo_timeout_publishes': timeout_publishes,
|
|
211
|
-
'centrifugo_avg_acks': avg_acks,
|
|
212
|
-
}
|
|
213
|
-
except Exception as e:
|
|
214
|
-
# Return zeros if Centrifugo models not available
|
|
215
|
-
return {
|
|
216
|
-
'centrifugo_total_publishes': 0,
|
|
217
|
-
'centrifugo_success_rate': 0,
|
|
218
|
-
'centrifugo_avg_duration': 0,
|
|
219
|
-
'centrifugo_failed_publishes': 0,
|
|
220
|
-
'centrifugo_timeout_publishes': 0,
|
|
221
|
-
'centrifugo_avg_acks': 0,
|
|
222
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Quick actions callbacks.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from typing import List
|
|
7
|
-
|
|
8
|
-
from django_cfg.modules.django_admin.icons import Icons
|
|
9
|
-
|
|
10
|
-
from ..models.dashboard import QuickAction
|
|
11
|
-
from .base import get_user_admin_urls
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ActionsCallbacks:
|
|
17
|
-
"""Quick actions callbacks."""
|
|
18
|
-
|
|
19
|
-
def get_quick_actions(self) -> List[QuickAction]:
|
|
20
|
-
"""Get quick action buttons as Pydantic models."""
|
|
21
|
-
# Get user admin URLs dynamically based on AUTH_USER_MODEL
|
|
22
|
-
user_admin_urls = get_user_admin_urls()
|
|
23
|
-
|
|
24
|
-
actions = [
|
|
25
|
-
QuickAction(
|
|
26
|
-
title="Add User",
|
|
27
|
-
description="Create new user account",
|
|
28
|
-
icon=Icons.PERSON_ADD,
|
|
29
|
-
link=user_admin_urls["add"],
|
|
30
|
-
color="primary",
|
|
31
|
-
category="admin",
|
|
32
|
-
),
|
|
33
|
-
QuickAction(
|
|
34
|
-
title="Support Tickets",
|
|
35
|
-
description="Manage support tickets",
|
|
36
|
-
icon=Icons.SUPPORT_AGENT,
|
|
37
|
-
link="admin:django_cfg_support_ticket_changelist",
|
|
38
|
-
color="primary",
|
|
39
|
-
category="support",
|
|
40
|
-
),
|
|
41
|
-
QuickAction(
|
|
42
|
-
title="Health Check",
|
|
43
|
-
description="System health status",
|
|
44
|
-
icon=Icons.HEALTH_AND_SAFETY,
|
|
45
|
-
link="/cfg/health/",
|
|
46
|
-
color="success",
|
|
47
|
-
category="system",
|
|
48
|
-
),
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
return actions
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Django Client (OpenAPI) integration callbacks.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from typing import Any, Dict, List, Tuple
|
|
7
|
-
|
|
8
|
-
from django.conf import settings
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class OpenAPIClientCallbacks:
|
|
14
|
-
"""Django Client (OpenAPI) integration callbacks."""
|
|
15
|
-
|
|
16
|
-
def get_openapi_groups_data(self) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
17
|
-
"""Get Django Client (OpenAPI) groups data."""
|
|
18
|
-
try:
|
|
19
|
-
# Get groups from OpenAPI service (includes default cfg group)
|
|
20
|
-
from django_cfg.modules.django_client.core import get_openapi_service
|
|
21
|
-
|
|
22
|
-
service = get_openapi_service()
|
|
23
|
-
if not service.config:
|
|
24
|
-
return [], {"total_apps": 0, "total_endpoints": 0, "total_groups": 0}
|
|
25
|
-
|
|
26
|
-
# Get groups with defaults (includes cfg group automatically)
|
|
27
|
-
groups_dict = service.get_groups()
|
|
28
|
-
groups_list = list(groups_dict.values())
|
|
29
|
-
api_prefix = getattr(service.config, "api_prefix", "api")
|
|
30
|
-
|
|
31
|
-
# Ensure urlconf modules AND URL patterns are created for all groups with apps
|
|
32
|
-
try:
|
|
33
|
-
from django.urls import path
|
|
34
|
-
from drf_spectacular.views import SpectacularAPIView
|
|
35
|
-
from django_cfg.modules.django_client.core.groups import GroupManager
|
|
36
|
-
from django_cfg.modules.django_client import urls as client_urls
|
|
37
|
-
|
|
38
|
-
manager = GroupManager(service.config)
|
|
39
|
-
for group in groups_list:
|
|
40
|
-
group_name = getattr(group, "name", "unknown") if not isinstance(group, dict) else group.get("name", "unknown")
|
|
41
|
-
apps = getattr(group, "apps", []) if not isinstance(group, dict) else group.get("apps", [])
|
|
42
|
-
group_version = getattr(group, "version", "1.0.0") if not isinstance(group, dict) else group.get("version", "1.0.0")
|
|
43
|
-
|
|
44
|
-
if apps:
|
|
45
|
-
try:
|
|
46
|
-
# Create urlconf module
|
|
47
|
-
manager.create_urlconf_module(group_name)
|
|
48
|
-
|
|
49
|
-
# Check if URL pattern already exists
|
|
50
|
-
url_name = f'openapi-schema-{group_name}'
|
|
51
|
-
url_exists = any(
|
|
52
|
-
hasattr(pattern, 'name') and pattern.name == url_name
|
|
53
|
-
for pattern in client_urls.urlpatterns
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Add URL pattern if it doesn't exist
|
|
57
|
-
if not url_exists:
|
|
58
|
-
new_pattern = path(
|
|
59
|
-
f'{group_name}/schema/',
|
|
60
|
-
SpectacularAPIView.as_view(
|
|
61
|
-
urlconf=f'_django_client_urlconf_{group_name}',
|
|
62
|
-
api_version=group_version,
|
|
63
|
-
),
|
|
64
|
-
name=url_name,
|
|
65
|
-
)
|
|
66
|
-
client_urls.urlpatterns.append(new_pattern)
|
|
67
|
-
except Exception:
|
|
68
|
-
pass # Silently skip if already exists or fails
|
|
69
|
-
except Exception:
|
|
70
|
-
pass # Silently skip if GroupManager fails
|
|
71
|
-
|
|
72
|
-
groups_data = []
|
|
73
|
-
total_apps = 0
|
|
74
|
-
total_endpoints = 0
|
|
75
|
-
|
|
76
|
-
for group in groups_list:
|
|
77
|
-
# Handle both dict and object access
|
|
78
|
-
if isinstance(group, dict):
|
|
79
|
-
group_name = group.get("name", "unknown")
|
|
80
|
-
title = group.get("title", group_name.title())
|
|
81
|
-
description = group.get("description", f"{group_name} group")
|
|
82
|
-
apps = group.get("apps", [])
|
|
83
|
-
else:
|
|
84
|
-
# Handle object access (for OpenAPIGroupConfig instances)
|
|
85
|
-
group_name = getattr(group, "name", "unknown")
|
|
86
|
-
title = getattr(group, "title", group_name.title())
|
|
87
|
-
description = getattr(group, "description", f"{group_name} group")
|
|
88
|
-
apps = getattr(group, "apps", [])
|
|
89
|
-
|
|
90
|
-
# Count actual endpoints by checking URL patterns (simplified estimate)
|
|
91
|
-
endpoint_count = len(apps) * 3 # Conservative estimate
|
|
92
|
-
|
|
93
|
-
groups_data.append({
|
|
94
|
-
"name": group_name,
|
|
95
|
-
"title": title,
|
|
96
|
-
"description": description,
|
|
97
|
-
"app_count": len(apps),
|
|
98
|
-
"endpoint_count": endpoint_count,
|
|
99
|
-
"status": "active",
|
|
100
|
-
"schema_url": f"/cfg/openapi/{group_name}/schema/",
|
|
101
|
-
"api_url": f"/{api_prefix}/{group_name}/",
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
total_apps += len(apps)
|
|
105
|
-
total_endpoints += endpoint_count
|
|
106
|
-
|
|
107
|
-
return groups_data, {
|
|
108
|
-
"total_apps": total_apps,
|
|
109
|
-
"total_endpoints": total_endpoints,
|
|
110
|
-
"total_groups": len(groups_list),
|
|
111
|
-
}
|
|
112
|
-
except Exception as e:
|
|
113
|
-
logger.error(f"Error getting OpenAPI groups: {e}")
|
|
114
|
-
return [], {
|
|
115
|
-
"total_apps": 0,
|
|
116
|
-
"total_endpoints": 0,
|
|
117
|
-
"total_groups": 0,
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Keep backward compatibility alias
|
|
122
|
-
RevolutionCallbacks = OpenAPIClientCallbacks
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Base utilities and helper functions for callbacks.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import importlib
|
|
6
|
-
import inspect
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Any, Dict, Set
|
|
9
|
-
|
|
10
|
-
from django.contrib.auth import get_user_model
|
|
11
|
-
from django.core.management import get_commands
|
|
12
|
-
from django.core.management.base import BaseCommand
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# Keywords in docstring/code that indicate command should not be in web UI
|
|
18
|
-
DANGEROUS_KEYWORDS: Set[str] = {
|
|
19
|
-
'questionary', 'input(', 'stdin', 'interactive', # Requires user input
|
|
20
|
-
'destructive', 'dangerous', 'irreversible', # Explicitly marked as dangerous
|
|
21
|
-
'runserver', 'testserver', # Dev servers
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
# Commands that should NEVER appear (absolute blacklist)
|
|
25
|
-
ABSOLUTE_BLACKLIST: Set[str] = {
|
|
26
|
-
# Destructive database commands
|
|
27
|
-
'flush', 'sqlflush', 'dbshell',
|
|
28
|
-
|
|
29
|
-
# Shell access (security risk)
|
|
30
|
-
'shell', 'shell_plus',
|
|
31
|
-
|
|
32
|
-
# Development server (not for web execution)
|
|
33
|
-
'runserver', 'testserver', 'runserver_ngrok',
|
|
34
|
-
|
|
35
|
-
# Project generation (not applicable in running app)
|
|
36
|
-
'startapp', 'startproject',
|
|
37
|
-
|
|
38
|
-
# SQL commands (direct SQL, potentially dangerous)
|
|
39
|
-
'sqlmigrate', 'sqlsequencereset',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def analyze_command_safety(command_class: BaseCommand, command_name: str) -> Dict[str, Any]:
|
|
44
|
-
"""
|
|
45
|
-
Analyze command to determine if it's safe for web execution.
|
|
46
|
-
|
|
47
|
-
Checks:
|
|
48
|
-
1. Explicit metadata (web_executable, requires_input, is_destructive)
|
|
49
|
-
2. Docstring analysis for dangerous keywords
|
|
50
|
-
3. Source code analysis for interactive input
|
|
51
|
-
4. Required arguments analysis
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
Dict with safety analysis results
|
|
55
|
-
"""
|
|
56
|
-
analysis = {
|
|
57
|
-
'is_safe': True,
|
|
58
|
-
'reasons': [],
|
|
59
|
-
'requires_input': False,
|
|
60
|
-
'is_destructive': False,
|
|
61
|
-
'web_executable': None,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Check explicit metadata (highest priority)
|
|
65
|
-
if hasattr(command_class, 'web_executable'):
|
|
66
|
-
analysis['web_executable'] = command_class.web_executable
|
|
67
|
-
if not command_class.web_executable:
|
|
68
|
-
analysis['is_safe'] = False
|
|
69
|
-
analysis['reasons'].append('Command explicitly marked as not web-executable')
|
|
70
|
-
return analysis
|
|
71
|
-
|
|
72
|
-
if hasattr(command_class, 'requires_input'):
|
|
73
|
-
analysis['requires_input'] = command_class.requires_input
|
|
74
|
-
if command_class.requires_input:
|
|
75
|
-
analysis['is_safe'] = False
|
|
76
|
-
analysis['reasons'].append('Command requires interactive input')
|
|
77
|
-
return analysis
|
|
78
|
-
|
|
79
|
-
if hasattr(command_class, 'is_destructive'):
|
|
80
|
-
analysis['is_destructive'] = command_class.is_destructive
|
|
81
|
-
if command_class.is_destructive:
|
|
82
|
-
analysis['is_safe'] = False
|
|
83
|
-
analysis['reasons'].append('Command is marked as destructive')
|
|
84
|
-
return analysis
|
|
85
|
-
|
|
86
|
-
# Analyze docstring for dangerous keywords
|
|
87
|
-
docstring = (inspect.getdoc(command_class) or '').lower()
|
|
88
|
-
for keyword in DANGEROUS_KEYWORDS:
|
|
89
|
-
if keyword in docstring:
|
|
90
|
-
analysis['is_safe'] = False
|
|
91
|
-
analysis['reasons'].append(f'Docstring contains dangerous keyword: {keyword}')
|
|
92
|
-
return analysis
|
|
93
|
-
|
|
94
|
-
# Analyze source code for interactive input
|
|
95
|
-
try:
|
|
96
|
-
source = inspect.getsource(command_class)
|
|
97
|
-
if 'questionary' in source or 'input(' in source:
|
|
98
|
-
analysis['is_safe'] = False
|
|
99
|
-
analysis['requires_input'] = True
|
|
100
|
-
analysis['reasons'].append('Command requires interactive user input')
|
|
101
|
-
return analysis
|
|
102
|
-
except Exception:
|
|
103
|
-
# Can't analyze source, be safe
|
|
104
|
-
pass
|
|
105
|
-
|
|
106
|
-
return analysis
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def is_command_allowed(command_name: str, app_name: str) -> bool:
|
|
110
|
-
"""
|
|
111
|
-
Check if command should be displayed in web interface.
|
|
112
|
-
|
|
113
|
-
Priority:
|
|
114
|
-
1. Custom blacklist from settings (DJANGO_CFG_COMMANDS_BLACKLIST)
|
|
115
|
-
2. Absolute blacklist - always exclude
|
|
116
|
-
3. Custom whitelist from settings (DJANGO_CFG_COMMANDS_WHITELIST)
|
|
117
|
-
4. Command metadata analysis (web_executable, requires_input, etc.)
|
|
118
|
-
5. django_cfg apps - analyze each command
|
|
119
|
-
6. Django core - be selective (only safe utility commands)
|
|
120
|
-
7. Third party - only if explicitly whitelisted
|
|
121
|
-
|
|
122
|
-
You can customize filtering in settings.py:
|
|
123
|
-
|
|
124
|
-
DJANGO_CFG_COMMANDS_BLACKLIST = {'my_dangerous_command'}
|
|
125
|
-
DJANGO_CFG_COMMANDS_WHITELIST = {'my_safe_command'}
|
|
126
|
-
|
|
127
|
-
Or add metadata to your Command class:
|
|
128
|
-
|
|
129
|
-
class Command(BaseCommand):
|
|
130
|
-
web_executable = True # Allow in web UI
|
|
131
|
-
requires_input = False # Doesn't need interactive input
|
|
132
|
-
is_destructive = False # Not destructive
|
|
133
|
-
"""
|
|
134
|
-
from django.conf import settings
|
|
135
|
-
|
|
136
|
-
# Custom blacklist from settings (highest priority)
|
|
137
|
-
custom_blacklist = getattr(settings, 'DJANGO_CFG_COMMANDS_BLACKLIST', set())
|
|
138
|
-
if command_name in custom_blacklist:
|
|
139
|
-
return False
|
|
140
|
-
|
|
141
|
-
# Absolute blacklist
|
|
142
|
-
if command_name in ABSOLUTE_BLACKLIST:
|
|
143
|
-
return False
|
|
144
|
-
|
|
145
|
-
# Custom whitelist from settings
|
|
146
|
-
custom_whitelist = getattr(settings, 'DJANGO_CFG_COMMANDS_WHITELIST', set())
|
|
147
|
-
if command_name in custom_whitelist:
|
|
148
|
-
return True
|
|
149
|
-
|
|
150
|
-
# Load and analyze command
|
|
151
|
-
try:
|
|
152
|
-
# Determine module path
|
|
153
|
-
if app_name == 'django_cfg':
|
|
154
|
-
module_path = f'django_cfg.management.commands.{command_name}'
|
|
155
|
-
elif app_name.startswith('django.'):
|
|
156
|
-
module_path = f'{app_name}.management.commands.{command_name}'
|
|
157
|
-
else:
|
|
158
|
-
module_path = f'{app_name}.management.commands.{command_name}'
|
|
159
|
-
|
|
160
|
-
command_module = importlib.import_module(module_path)
|
|
161
|
-
if hasattr(command_module, 'Command'):
|
|
162
|
-
command_class = command_module.Command
|
|
163
|
-
|
|
164
|
-
# Analyze command safety
|
|
165
|
-
analysis = analyze_command_safety(command_class, command_name)
|
|
166
|
-
|
|
167
|
-
# If command has explicit web_executable metadata, use it
|
|
168
|
-
if analysis['web_executable'] is not None:
|
|
169
|
-
return analysis['web_executable']
|
|
170
|
-
|
|
171
|
-
# If analysis says it's unsafe, exclude
|
|
172
|
-
if not analysis['is_safe']:
|
|
173
|
-
logger.debug(f"Command {command_name} excluded: {', '.join(analysis['reasons'])}")
|
|
174
|
-
return False
|
|
175
|
-
except Exception as e:
|
|
176
|
-
# Can't load/analyze command
|
|
177
|
-
logger.debug(f"Could not analyze command {command_name}: {e}")
|
|
178
|
-
# Be conservative - if we can't analyze, exclude unless from trusted source
|
|
179
|
-
pass
|
|
180
|
-
|
|
181
|
-
# Django CFG commands - include if analysis passed
|
|
182
|
-
if app_name == 'django_cfg':
|
|
183
|
-
return True
|
|
184
|
-
|
|
185
|
-
# Safe Django core commands (utility/read-only)
|
|
186
|
-
safe_django_core = {
|
|
187
|
-
'check', 'diffsettings', 'showmigrations',
|
|
188
|
-
'createcachetable', 'sendtestemail',
|
|
189
|
-
}
|
|
190
|
-
if app_name.startswith('django.') and command_name in safe_django_core:
|
|
191
|
-
return True
|
|
192
|
-
|
|
193
|
-
# Exclude other Django core by default
|
|
194
|
-
if app_name.startswith('django.'):
|
|
195
|
-
return False
|
|
196
|
-
|
|
197
|
-
# Third-party apps - only whitelisted
|
|
198
|
-
return False
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def get_available_commands():
|
|
202
|
-
"""Get all available Django management commands (filtered for safety)."""
|
|
203
|
-
commands_dict = get_commands()
|
|
204
|
-
commands_list = []
|
|
205
|
-
|
|
206
|
-
for command_name, app_name in commands_dict.items():
|
|
207
|
-
# Filter out unsafe/unwanted commands
|
|
208
|
-
if not is_command_allowed(command_name, app_name):
|
|
209
|
-
continue
|
|
210
|
-
|
|
211
|
-
try:
|
|
212
|
-
# Try to get command description
|
|
213
|
-
if app_name == 'django_cfg':
|
|
214
|
-
module_path = f'django_cfg.management.commands.{command_name}'
|
|
215
|
-
else:
|
|
216
|
-
module_path = f'{app_name}.management.commands.{command_name}'
|
|
217
|
-
|
|
218
|
-
try:
|
|
219
|
-
command_module = importlib.import_module(module_path)
|
|
220
|
-
if hasattr(command_module, 'Command'):
|
|
221
|
-
command_class = command_module.Command
|
|
222
|
-
description = getattr(command_class, 'help', f'{command_name} command')
|
|
223
|
-
else:
|
|
224
|
-
description = f'{command_name} command'
|
|
225
|
-
except ImportError:
|
|
226
|
-
description = f'{command_name} command'
|
|
227
|
-
|
|
228
|
-
commands_list.append({
|
|
229
|
-
'name': command_name,
|
|
230
|
-
'app': app_name,
|
|
231
|
-
'description': description,
|
|
232
|
-
'is_core': app_name.startswith('django.'),
|
|
233
|
-
'is_custom': app_name == 'django_cfg',
|
|
234
|
-
})
|
|
235
|
-
except Exception as e:
|
|
236
|
-
# Skip problematic commands
|
|
237
|
-
logger.debug(f"Skipping command {command_name}: {e}")
|
|
238
|
-
continue
|
|
239
|
-
|
|
240
|
-
return commands_list
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def get_commands_by_category():
|
|
244
|
-
"""Get commands categorized by type."""
|
|
245
|
-
commands = get_available_commands()
|
|
246
|
-
|
|
247
|
-
categorized = {
|
|
248
|
-
'django_cfg': [],
|
|
249
|
-
'django_core': [],
|
|
250
|
-
'third_party': [],
|
|
251
|
-
'project': [],
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
for cmd in commands:
|
|
255
|
-
if cmd['app'] == 'django_cfg':
|
|
256
|
-
categorized['django_cfg'].append(cmd)
|
|
257
|
-
elif cmd['app'].startswith('django.'):
|
|
258
|
-
categorized['django_core'].append(cmd)
|
|
259
|
-
elif cmd['app'].startswith(('src.', 'api.', 'accounts.')):
|
|
260
|
-
categorized['project'].append(cmd)
|
|
261
|
-
else:
|
|
262
|
-
categorized['third_party'].append(cmd)
|
|
263
|
-
|
|
264
|
-
return categorized
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def get_user_admin_urls():
|
|
268
|
-
"""Get admin URLs for user model."""
|
|
269
|
-
try:
|
|
270
|
-
User = get_user_model()
|
|
271
|
-
|
|
272
|
-
app_label = User._meta.app_label
|
|
273
|
-
model_name = User._meta.model_name
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
'changelist': f'admin:{app_label}_{model_name}_changelist',
|
|
277
|
-
'add': f'admin:{app_label}_{model_name}_add',
|
|
278
|
-
'change': f'admin:{app_label}_{model_name}_change/{{id}}/',
|
|
279
|
-
'delete': f'admin:{app_label}_{model_name}_delete/{{id}}/',
|
|
280
|
-
'view': f'admin:{app_label}_{model_name}_view/{{id}}/',
|
|
281
|
-
}
|
|
282
|
-
except Exception:
|
|
283
|
-
# Universal fallback - return admin index for all actions
|
|
284
|
-
return {
|
|
285
|
-
'changelist': 'admin:index',
|
|
286
|
-
'add': 'admin:index',
|
|
287
|
-
'change': 'admin:index',
|
|
288
|
-
'delete': 'admin:index',
|
|
289
|
-
'view': 'admin:index',
|
|
290
|
-
}
|