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.

Files changed (146) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/centrifugo/views/__init__.py +0 -2
  3. django_cfg/apps/dashboard/services/__init__.py +2 -0
  4. django_cfg/apps/dashboard/services/overview_service.py +205 -0
  5. django_cfg/apps/frontend/test_routing.py +134 -0
  6. django_cfg/apps/frontend/views.py +69 -28
  7. django_cfg/apps/urls.py +0 -1
  8. django_cfg/core/builders/apps_builder.py +0 -58
  9. django_cfg/modules/django_unfold/__init__.py +5 -24
  10. django_cfg/modules/django_unfold/models/__init__.py +0 -23
  11. django_cfg/modules/django_unfold/models/config.py +11 -65
  12. django_cfg/modules/django_unfold/{dashboard.py → navigation.py} +21 -152
  13. django_cfg/modules/django_unfold/tailwind.py +2 -4
  14. django_cfg/pyproject.toml +1 -1
  15. django_cfg/registry/third_party.py +0 -9
  16. django_cfg/routing/callbacks.py +1 -43
  17. django_cfg/static/frontend/admin/404/index.html +1 -1
  18. django_cfg/static/frontend/admin/404.html +1 -1
  19. django_cfg/static/frontend/admin/500/index.html +1 -1
  20. django_cfg/static/frontend/admin/_next/static/{D_d9HRw5Yn7BRHAX5q89_ → 0sN9ktsgXH48ygtGSrhfu}/_buildManifest.js +1 -1
  21. django_cfg/static/frontend/admin/_next/static/chunks/50314-9443faa6df24aebf.js +1 -0
  22. django_cfg/static/frontend/admin/_next/static/chunks/pages/{_app-1c0fff0f59a6d683.js → _app-c7dcd3aa616fab68.js} +1 -1
  23. django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{centrifugo-44a8313fa040e9ad.js → centrifugo-f9ecbc3ae0052a03.js} +1 -1
  24. django_cfg/static/frontend/admin/auth/index.html +1 -1
  25. django_cfg/static/frontend/admin/index.html +1 -1
  26. django_cfg/static/frontend/admin/legal/cookies/index.html +1 -1
  27. django_cfg/static/frontend/admin/legal/privacy/index.html +1 -1
  28. django_cfg/static/frontend/admin/legal/security/index.html +1 -1
  29. django_cfg/static/frontend/admin/legal/terms/index.html +1 -1
  30. django_cfg/static/frontend/admin/private/centrifugo/index.html +1 -1
  31. django_cfg/static/frontend/admin/private/index.html +1 -1
  32. django_cfg/static/frontend/admin/private/profile/index.html +1 -1
  33. django_cfg/static/frontend/admin/private/ui/index.html +2 -2
  34. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/METADATA +1 -1
  35. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/RECORD +39 -143
  36. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +0 -260
  37. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +0 -313
  38. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +0 -803
  39. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +0 -341
  40. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +0 -432
  41. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +0 -33
  42. django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +0 -210
  43. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +0 -46
  44. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +0 -123
  45. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +0 -45
  46. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +0 -84
  47. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/stat_cards.html +0 -53
  48. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +0 -91
  49. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/tab_navigation.html +0 -29
  50. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +0 -415
  51. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +0 -61
  52. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +0 -58
  53. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +0 -48
  54. django_cfg/apps/centrifugo/templatetags/__init__.py +0 -1
  55. django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +0 -81
  56. django_cfg/apps/centrifugo/urls_admin.py +0 -20
  57. django_cfg/apps/centrifugo/views/dashboard.py +0 -28
  58. django_cfg/modules/django_dashboard/__init__.py +0 -23
  59. django_cfg/modules/django_dashboard/components.py +0 -312
  60. django_cfg/modules/django_dashboard/debug.py +0 -174
  61. django_cfg/modules/django_dashboard/management/__init__.py +0 -0
  62. django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
  63. django_cfg/modules/django_dashboard/management/commands/debug_dashboard.py +0 -109
  64. django_cfg/modules/django_dashboard/sections/__init__.py +0 -1
  65. django_cfg/modules/django_dashboard/sections/base.py +0 -129
  66. django_cfg/modules/django_dashboard/sections/commands.py +0 -33
  67. django_cfg/modules/django_dashboard/sections/documentation.py +0 -393
  68. django_cfg/modules/django_dashboard/sections/overview.py +0 -398
  69. django_cfg/modules/django_dashboard/sections/stats.py +0 -48
  70. django_cfg/modules/django_dashboard/sections/system.py +0 -74
  71. django_cfg/modules/django_dashboard/sections/widgets.py +0 -222
  72. django_cfg/modules/django_unfold/callbacks/__init__.py +0 -9
  73. django_cfg/modules/django_unfold/callbacks/actions.py +0 -51
  74. django_cfg/modules/django_unfold/callbacks/apizones.py +0 -122
  75. django_cfg/modules/django_unfold/callbacks/base.py +0 -290
  76. django_cfg/modules/django_unfold/callbacks/charts.py +0 -223
  77. django_cfg/modules/django_unfold/callbacks/commands.py +0 -40
  78. django_cfg/modules/django_unfold/callbacks/main.py +0 -322
  79. django_cfg/modules/django_unfold/callbacks/statistics.py +0 -240
  80. django_cfg/modules/django_unfold/callbacks/system.py +0 -180
  81. django_cfg/modules/django_unfold/callbacks/users.py +0 -65
  82. django_cfg/modules/django_unfold/models/dashboard.py +0 -207
  83. django_cfg/modules/django_unfold/models/tabs.py +0 -26
  84. django_cfg/modules/django_unfold/models.py +0 -98
  85. django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +0 -102
  86. django_cfg/static/frontend/admin/_next/static/chunks/50314-5ec79b293c2283dd.js +0 -1
  87. django_cfg/templates/admin/sections/commands_section.html +0 -5
  88. django_cfg/templates/admin/sections/documentation_section.html +0 -5
  89. django_cfg/templates/admin/sections/overview_section.html +0 -5
  90. django_cfg/templates/admin/sections/stats_section.html +0 -5
  91. django_cfg/templates/admin/sections/system_section.html +0 -5
  92. django_cfg/templates/admin/sections/widgets_section.html +0 -11
  93. django_cfg/templates/admin_old/components/action_grid.html +0 -49
  94. django_cfg/templates/admin_old/components/card.html +0 -50
  95. django_cfg/templates/admin_old/components/data_table.html +0 -67
  96. django_cfg/templates/admin_old/components/metric_card.html +0 -39
  97. django_cfg/templates/admin_old/components/modal.html +0 -58
  98. django_cfg/templates/admin_old/components/progress_bar.html +0 -20
  99. django_cfg/templates/admin_old/components/section_header.html +0 -26
  100. django_cfg/templates/admin_old/components/stat_item.html +0 -32
  101. django_cfg/templates/admin_old/components/stats_grid.html +0 -72
  102. django_cfg/templates/admin_old/components/status_badge.html +0 -28
  103. django_cfg/templates/admin_old/components/user_avatar.html +0 -27
  104. django_cfg/templates/admin_old/constance/change_list.html +0 -74
  105. django_cfg/templates/admin_old/constance/includes/default_value.html +0 -24
  106. django_cfg/templates/admin_old/constance/includes/fieldset_header.html +0 -15
  107. django_cfg/templates/admin_old/constance/includes/results_list.html +0 -16
  108. django_cfg/templates/admin_old/constance/includes/setting_row.html +0 -50
  109. django_cfg/templates/admin_old/constance/includes/table_headers.html +0 -10
  110. django_cfg/templates/admin_old/examples/component_class_example.html +0 -156
  111. django_cfg/templates/admin_old/import_export/change_list_export.html +0 -24
  112. django_cfg/templates/admin_old/import_export/change_list_import.html +0 -24
  113. django_cfg/templates/admin_old/import_export/change_list_import_export.html +0 -34
  114. django_cfg/templates/admin_old/index.html +0 -80
  115. django_cfg/templates/admin_old/index_new.html +0 -119
  116. django_cfg/templates/admin_old/layouts/base_dashboard.html +0 -62
  117. django_cfg/templates/admin_old/layouts/dashboard_with_tabs.html +0 -176
  118. django_cfg/templates/admin_old/sections/commands_section.html +0 -549
  119. django_cfg/templates/admin_old/sections/documentation_section.html +0 -152
  120. django_cfg/templates/admin_old/sections/overview_section.html +0 -112
  121. django_cfg/templates/admin_old/sections/stats_section.html +0 -35
  122. django_cfg/templates/admin_old/sections/system_section.html +0 -99
  123. django_cfg/templates/admin_old/sections/widgets_section.html +0 -129
  124. django_cfg/templates/admin_old/snippets/components/activity_tracker.html +0 -70
  125. django_cfg/templates/admin_old/snippets/components/charts_section.html +0 -113
  126. django_cfg/templates/admin_old/snippets/components/django_commands.html +0 -270
  127. django_cfg/templates/admin_old/snippets/components/quick_actions.html +0 -66
  128. django_cfg/templates/admin_old/snippets/components/recent_activity_improved.html +0 -25
  129. django_cfg/templates/admin_old/snippets/components/recent_users_table.html +0 -102
  130. django_cfg/templates/admin_old/snippets/components/stats_cards.html +0 -4
  131. django_cfg/templates/admin_old/snippets/components/stats_tiles.html +0 -92
  132. django_cfg/templates/admin_old/snippets/components/system_health.html +0 -22
  133. django_cfg/templates/admin_old/snippets/components/system_metrics.html +0 -199
  134. django_cfg/templates/admin_old/snippets/components/user_permissions.html +0 -57
  135. django_cfg/templates/admin_old/snippets/tabs/app_stats_tab.html +0 -201
  136. django_cfg/templates/admin_old/snippets/tabs/commands_tab.html +0 -114
  137. django_cfg/templates/admin_old/snippets/tabs/documentation_tab.html +0 -42
  138. django_cfg/templates/admin_old/snippets/tabs/overview_tab.html +0 -116
  139. django_cfg/templates/admin_old/snippets/tabs/stats_tab.html +0 -89
  140. django_cfg/templates/admin_old/snippets/tabs/users_tab.html +0 -51
  141. django_cfg/templates/admin_old/snippets/tabs/widgets_tab.html +0 -38
  142. django_cfg/templates/admin_old/snippets/zones/zones_table.html +0 -176
  143. /django_cfg/static/frontend/admin/_next/static/{D_d9HRw5Yn7BRHAX5q89_ → 0sN9ktsgXH48ygtGSrhfu}/_ssgManifest.js +0 -0
  144. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/WHEEL +0 -0
  145. {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/entry_points.txt +0 -0
  146. {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,9 +0,0 @@
1
- """
2
- Django CFG Unfold Callbacks Module
3
-
4
- Modular callback system for Django Unfold dashboard.
5
- """
6
-
7
- from .main import UnfoldCallbacks
8
-
9
- __all__ = ['UnfoldCallbacks']
@@ -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
- }