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
django_cfg/__init__.py
CHANGED
|
@@ -3,7 +3,6 @@ Views for Centrifugo module.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from .admin_api import CentrifugoAdminAPIViewSet
|
|
6
|
-
from .dashboard import dashboard_view
|
|
7
6
|
from .monitoring import CentrifugoMonitorViewSet
|
|
8
7
|
from .testing_api import CentrifugoTestingAPIViewSet
|
|
9
8
|
|
|
@@ -11,5 +10,4 @@ __all__ = [
|
|
|
11
10
|
'CentrifugoMonitorViewSet',
|
|
12
11
|
'CentrifugoAdminAPIViewSet',
|
|
13
12
|
'CentrifugoTestingAPIViewSet',
|
|
14
|
-
'dashboard_view',
|
|
15
13
|
]
|
|
@@ -10,6 +10,7 @@ from .system_health_service import SystemHealthService
|
|
|
10
10
|
from .charts_service import ChartsService
|
|
11
11
|
from .commands_service import CommandsService
|
|
12
12
|
from .apizones_service import APIZonesService
|
|
13
|
+
from .overview_service import OverviewService
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
'StatisticsService',
|
|
@@ -17,4 +18,5 @@ __all__ = [
|
|
|
17
18
|
'ChartsService',
|
|
18
19
|
'CommandsService',
|
|
19
20
|
'APIZonesService',
|
|
21
|
+
'OverviewService',
|
|
20
22
|
]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overview Service
|
|
3
|
+
|
|
4
|
+
Provides overview data including recent users and activity tracking.
|
|
5
|
+
Extracted from django_dashboard for API-based dashboard.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import timedelta
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
|
+
|
|
12
|
+
from django.contrib.auth import get_user_model
|
|
13
|
+
from django.utils import timezone
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OverviewService:
|
|
19
|
+
"""
|
|
20
|
+
Service for dashboard overview data.
|
|
21
|
+
|
|
22
|
+
%%PRIORITY:HIGH%%
|
|
23
|
+
%%AI_HINT: Provides recent activity and user registration tracking%%
|
|
24
|
+
|
|
25
|
+
TAGS: overview, dashboard, activity, service
|
|
26
|
+
DEPENDS_ON: [django.contrib.auth]
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initialize overview service."""
|
|
31
|
+
self.logger = logger
|
|
32
|
+
|
|
33
|
+
def _get_user_model(self):
|
|
34
|
+
"""Get the user model safely."""
|
|
35
|
+
return get_user_model()
|
|
36
|
+
|
|
37
|
+
def get_recent_users(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
38
|
+
"""
|
|
39
|
+
Get recent users for activity section.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
limit: Maximum number of users to return (default: 10)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of user dictionaries with id, username, email, is_active, date_joined
|
|
46
|
+
|
|
47
|
+
%%AI_HINT: Real Django ORM query for recent users%%
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
User = self._get_user_model()
|
|
51
|
+
recent_users = User.objects.order_by('-date_joined')[:limit]
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
'id': user.id,
|
|
56
|
+
'username': user.username,
|
|
57
|
+
'email': user.email or '',
|
|
58
|
+
'is_active': user.is_active,
|
|
59
|
+
'is_staff': user.is_staff,
|
|
60
|
+
'date_joined': user.date_joined.isoformat() if user.date_joined else None,
|
|
61
|
+
}
|
|
62
|
+
for user in recent_users
|
|
63
|
+
]
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.logger.error(f"Error getting recent users: {e}")
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
def get_activity_tracker(self, days: int = 365) -> List[Dict[str, Any]]:
|
|
69
|
+
"""
|
|
70
|
+
Get activity tracker data for GitHub-style heatmap.
|
|
71
|
+
|
|
72
|
+
Returns list of dicts with date and count for last N days.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
days: Number of days to track (default: 365)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of activity data: [{'date': 'YYYY-MM-DD', 'count': int, 'level': int}, ...]
|
|
79
|
+
level: 0-4 (0=no activity, 4=very high activity)
|
|
80
|
+
|
|
81
|
+
%%AI_HINT: Generates daily activity counts from user registrations and logins%%
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
User = self._get_user_model()
|
|
85
|
+
today = timezone.now().date()
|
|
86
|
+
activity_data = []
|
|
87
|
+
|
|
88
|
+
for days_ago in range(days - 1, -1, -1): # Newest last
|
|
89
|
+
date = today - timedelta(days=days_ago)
|
|
90
|
+
|
|
91
|
+
# Count user registrations on this day
|
|
92
|
+
registrations = User.objects.filter(
|
|
93
|
+
date_joined__date=date
|
|
94
|
+
).count()
|
|
95
|
+
|
|
96
|
+
# Count logins on this day (if last_login exists)
|
|
97
|
+
logins = 0
|
|
98
|
+
if hasattr(User, 'last_login'):
|
|
99
|
+
logins = User.objects.filter(
|
|
100
|
+
last_login__date=date
|
|
101
|
+
).count()
|
|
102
|
+
|
|
103
|
+
# Total activity for the day
|
|
104
|
+
total_activity = registrations + logins
|
|
105
|
+
|
|
106
|
+
activity_data.append({
|
|
107
|
+
'date': date.isoformat(),
|
|
108
|
+
'count': total_activity,
|
|
109
|
+
'level': self._get_activity_level(total_activity),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return activity_data
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.logger.error(f"Error getting activity tracker: {e}")
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
def _get_activity_level(self, count: int) -> int:
|
|
119
|
+
"""
|
|
120
|
+
Convert activity count to level (0-4) for heatmap colors.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
count: Number of activities
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Activity level (0-4):
|
|
127
|
+
- 0 = no activity (gray)
|
|
128
|
+
- 1 = low (light green)
|
|
129
|
+
- 2 = medium (green)
|
|
130
|
+
- 3 = high (dark green)
|
|
131
|
+
- 4 = very high (darkest green)
|
|
132
|
+
"""
|
|
133
|
+
if count == 0:
|
|
134
|
+
return 0
|
|
135
|
+
elif count <= 2:
|
|
136
|
+
return 1
|
|
137
|
+
elif count <= 5:
|
|
138
|
+
return 2
|
|
139
|
+
elif count <= 10:
|
|
140
|
+
return 3
|
|
141
|
+
else:
|
|
142
|
+
return 4
|
|
143
|
+
|
|
144
|
+
def get_key_stats(self) -> Dict[str, Any]:
|
|
145
|
+
"""
|
|
146
|
+
Get key statistics for overview.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dictionary with users, databases, apps counts
|
|
150
|
+
|
|
151
|
+
%%AI_HINT: Provides high-level system statistics%%
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
User = self._get_user_model()
|
|
155
|
+
|
|
156
|
+
# Get database count
|
|
157
|
+
from django.db import connection
|
|
158
|
+
db_count = len(connection.settings_dict.get('DATABASES', {})) if hasattr(connection, 'settings_dict') else 1
|
|
159
|
+
|
|
160
|
+
# Get app count
|
|
161
|
+
from django.apps import apps
|
|
162
|
+
app_count = len(apps.get_app_configs())
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
'users': User.objects.count(),
|
|
166
|
+
'databases': db_count,
|
|
167
|
+
'apps': app_count,
|
|
168
|
+
}
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self.logger.error(f"Error getting key stats: {e}")
|
|
171
|
+
return {
|
|
172
|
+
'users': 0,
|
|
173
|
+
'databases': 0,
|
|
174
|
+
'apps': 0,
|
|
175
|
+
'error': str(e)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def get_system_info(self) -> Dict[str, Any]:
|
|
179
|
+
"""
|
|
180
|
+
Get system information.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Dictionary with Python version and system metrics
|
|
184
|
+
|
|
185
|
+
%%AI_HINT: System-level information using psutil%%
|
|
186
|
+
"""
|
|
187
|
+
import sys
|
|
188
|
+
import psutil
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
return {
|
|
192
|
+
'python_version': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
193
|
+
'cpu_percent': round(psutil.cpu_percent(interval=0.1), 1),
|
|
194
|
+
'memory_percent': round(psutil.virtual_memory().percent, 1),
|
|
195
|
+
'disk_percent': round(psutil.disk_usage('/').percent, 1),
|
|
196
|
+
}
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self.logger.error(f"Error getting system info: {e}")
|
|
199
|
+
return {
|
|
200
|
+
'python_version': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
201
|
+
'cpu_percent': 0,
|
|
202
|
+
'memory_percent': 0,
|
|
203
|
+
'disk_percent': 0,
|
|
204
|
+
'error': str(e)
|
|
205
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test script for SPA routing logic.
|
|
3
|
+
|
|
4
|
+
Run this to verify that URL paths resolve correctly to static files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import django_cfg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_resolve_spa_path():
|
|
12
|
+
"""Test the SPA path resolution logic."""
|
|
13
|
+
|
|
14
|
+
# Simulate the base directory
|
|
15
|
+
base_dir = Path(django_cfg.__file__).parent / 'static' / 'frontend' / 'admin'
|
|
16
|
+
|
|
17
|
+
print(f"Base directory: {base_dir}")
|
|
18
|
+
print(f"Base directory exists: {base_dir.exists()}\n")
|
|
19
|
+
|
|
20
|
+
# Test cases
|
|
21
|
+
test_cases = [
|
|
22
|
+
# (input_path, expected_output_path, description)
|
|
23
|
+
('', 'index.html', 'Root path'),
|
|
24
|
+
('/', 'index.html', 'Root path with slash'),
|
|
25
|
+
('private/centrifugo', 'private/centrifugo/index.html', 'Nested route without trailing slash'),
|
|
26
|
+
('private/centrifugo/', 'private/centrifugo/index.html', 'Nested route with trailing slash'),
|
|
27
|
+
('private', 'private.html', 'Single segment route'),
|
|
28
|
+
('private/', 'private/index.html', 'Single segment with trailing slash'),
|
|
29
|
+
('_next/static/chunks/app.js', '_next/static/chunks/app.js', 'Static asset (exact match)'),
|
|
30
|
+
('favicon.ico', 'favicon.ico', 'Favicon (exact match)'),
|
|
31
|
+
('unknown/route', 'index.html', 'Unknown route (SPA fallback)'),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def resolve_spa_path(base_dir, path):
|
|
35
|
+
"""
|
|
36
|
+
Exact copy of the view's _resolve_spa_path method.
|
|
37
|
+
|
|
38
|
+
This should match the logic in views.py exactly.
|
|
39
|
+
"""
|
|
40
|
+
# Handle empty path (done in view before calling this method)
|
|
41
|
+
if not path or path == '/':
|
|
42
|
+
path = 'index.html'
|
|
43
|
+
return path
|
|
44
|
+
|
|
45
|
+
file_path = base_dir / path
|
|
46
|
+
path_normalized = path.rstrip('/')
|
|
47
|
+
|
|
48
|
+
# Strategy 1: Exact file match (for static assets like JS, CSS, images)
|
|
49
|
+
if file_path.exists() and file_path.is_file():
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
# Strategy 2: Try path/index.html (most common for SPA routes)
|
|
53
|
+
index_in_dir = base_dir / path_normalized / 'index.html'
|
|
54
|
+
if index_in_dir.exists():
|
|
55
|
+
resolved_path = f"{path_normalized}/index.html"
|
|
56
|
+
return resolved_path
|
|
57
|
+
|
|
58
|
+
# Strategy 3: Try with trailing slash + index.html
|
|
59
|
+
if path.endswith('/'):
|
|
60
|
+
index_path = path + 'index.html'
|
|
61
|
+
if (base_dir / index_path).exists():
|
|
62
|
+
return index_path
|
|
63
|
+
|
|
64
|
+
# Strategy 4: Try path.html (Next.js static export behavior)
|
|
65
|
+
html_file = base_dir / (path_normalized + '.html')
|
|
66
|
+
if html_file.exists():
|
|
67
|
+
resolved_path = path_normalized + '.html'
|
|
68
|
+
return resolved_path
|
|
69
|
+
|
|
70
|
+
# Strategy 5: Check if it's a directory without index.html
|
|
71
|
+
if file_path.exists() and file_path.is_dir():
|
|
72
|
+
# Try index.html in that directory
|
|
73
|
+
index_in_existing_dir = file_path / 'index.html'
|
|
74
|
+
if index_in_existing_dir.exists():
|
|
75
|
+
resolved_path = f"{path_normalized}/index.html"
|
|
76
|
+
return resolved_path
|
|
77
|
+
|
|
78
|
+
# Strategy 6: SPA fallback - serve root index.html
|
|
79
|
+
# This allows client-side routing to handle unknown routes
|
|
80
|
+
root_index = base_dir / 'index.html'
|
|
81
|
+
if root_index.exists():
|
|
82
|
+
return 'index.html'
|
|
83
|
+
|
|
84
|
+
# Strategy 7: Nothing found - return original path (will 404)
|
|
85
|
+
return path
|
|
86
|
+
|
|
87
|
+
print("=" * 80)
|
|
88
|
+
print("SPA ROUTING TEST RESULTS")
|
|
89
|
+
print("=" * 80)
|
|
90
|
+
|
|
91
|
+
passed = 0
|
|
92
|
+
failed = 0
|
|
93
|
+
|
|
94
|
+
for input_path, expected, description in test_cases:
|
|
95
|
+
result = resolve_spa_path(base_dir, input_path)
|
|
96
|
+
|
|
97
|
+
# Check if file exists
|
|
98
|
+
resolved_file = base_dir / result
|
|
99
|
+
file_exists = resolved_file.exists() and resolved_file.is_file()
|
|
100
|
+
|
|
101
|
+
# Determine test status
|
|
102
|
+
if result == expected:
|
|
103
|
+
status = "✅ PASS"
|
|
104
|
+
passed += 1
|
|
105
|
+
else:
|
|
106
|
+
status = "❌ FAIL"
|
|
107
|
+
failed += 1
|
|
108
|
+
|
|
109
|
+
print(f"\n{status} - {description}")
|
|
110
|
+
print(f" Input: '{input_path}'")
|
|
111
|
+
print(f" Expected: '{expected}'")
|
|
112
|
+
print(f" Got: '{result}'")
|
|
113
|
+
print(f" File exists: {file_exists}")
|
|
114
|
+
|
|
115
|
+
if result != expected:
|
|
116
|
+
print(f" ⚠️ Mismatch detected!")
|
|
117
|
+
|
|
118
|
+
print("\n" + "=" * 80)
|
|
119
|
+
print(f"TEST SUMMARY: {passed} passed, {failed} failed out of {len(test_cases)} tests")
|
|
120
|
+
print("=" * 80)
|
|
121
|
+
|
|
122
|
+
# Additional info
|
|
123
|
+
print("\n📁 File structure check:")
|
|
124
|
+
if base_dir.exists():
|
|
125
|
+
print(f" - index.html: {(base_dir / 'index.html').exists()}")
|
|
126
|
+
print(f" - private.html: {(base_dir / 'private.html').exists()}")
|
|
127
|
+
print(f" - private/index.html: {(base_dir / 'private' / 'index.html').exists()}")
|
|
128
|
+
print(f" - private/centrifugo/index.html: {(base_dir / 'private' / 'centrifugo' / 'index.html').exists()}")
|
|
129
|
+
else:
|
|
130
|
+
print(f" ⚠️ Base directory does not exist!")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == '__main__':
|
|
134
|
+
test_resolve_spa_path()
|
|
@@ -55,34 +55,8 @@ class NextJSStaticView(View):
|
|
|
55
55
|
path = 'index.html'
|
|
56
56
|
logger.debug(f"Root path requested, serving: {path}")
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
path_without_slash = path.rstrip('/')
|
|
61
|
-
|
|
62
|
-
# Strategy 1: Try directory/index.html (most common for static servers)
|
|
63
|
-
index_path = path + 'index.html'
|
|
64
|
-
if (base_dir / index_path).exists():
|
|
65
|
-
path = index_path
|
|
66
|
-
# Strategy 2: Try path.html (Next.js static export behavior)
|
|
67
|
-
elif (base_dir / (path_without_slash + '.html')).exists():
|
|
68
|
-
path = path_without_slash + '.html'
|
|
69
|
-
# Strategy 3: Keep as directory path (will fail later if directory doesn't exist)
|
|
70
|
-
else:
|
|
71
|
-
path = index_path # Default to index.html in directory
|
|
72
|
-
|
|
73
|
-
# For routes without extension, try .html (Next.js static export behavior)
|
|
74
|
-
file_path = base_dir / path
|
|
75
|
-
if not file_path.exists() and not path.endswith('.html') and '.' not in Path(path).name:
|
|
76
|
-
html_path = path + '.html'
|
|
77
|
-
html_file = base_dir / html_path
|
|
78
|
-
if html_file.exists():
|
|
79
|
-
path = html_path
|
|
80
|
-
else:
|
|
81
|
-
# Try path/index.html as fallback
|
|
82
|
-
index_path = path + '/index.html'
|
|
83
|
-
index_file = base_dir / index_path
|
|
84
|
-
if index_file.exists():
|
|
85
|
-
path = index_path
|
|
58
|
+
# Resolve file path with SPA routing fallback strategy
|
|
59
|
+
path = self._resolve_spa_path(base_dir, path)
|
|
86
60
|
|
|
87
61
|
# For HTML files, remove conditional GET headers to force full response
|
|
88
62
|
# This allows JWT token injection (can't inject into 304 Not Modified responses)
|
|
@@ -116,6 +90,73 @@ class NextJSStaticView(View):
|
|
|
116
90
|
|
|
117
91
|
return response
|
|
118
92
|
|
|
93
|
+
def _resolve_spa_path(self, base_dir, path):
|
|
94
|
+
"""
|
|
95
|
+
Resolve SPA path with multiple fallback strategies.
|
|
96
|
+
|
|
97
|
+
Resolution order:
|
|
98
|
+
1. Exact file match (e.g., script.js, style.css)
|
|
99
|
+
2. path/index.html (e.g., private/centrifugo/index.html)
|
|
100
|
+
3. path.html (e.g., private.html for /private)
|
|
101
|
+
4. Fallback to root index.html for SPA routing
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
/private/centrifugo → private/centrifugo/index.html
|
|
105
|
+
/private → private.html OR private/index.html
|
|
106
|
+
/_next/static/... → _next/static/... (exact match)
|
|
107
|
+
/unknown/route → index.html (SPA fallback)
|
|
108
|
+
"""
|
|
109
|
+
file_path = base_dir / path
|
|
110
|
+
|
|
111
|
+
# Remove trailing slash for processing
|
|
112
|
+
path_normalized = path.rstrip('/')
|
|
113
|
+
|
|
114
|
+
# Strategy 1: Exact file match (for static assets like JS, CSS, images)
|
|
115
|
+
if file_path.exists() and file_path.is_file():
|
|
116
|
+
logger.debug(f"[SPA Router] Exact match: {path}")
|
|
117
|
+
return path
|
|
118
|
+
|
|
119
|
+
# Strategy 2: Try path/index.html (most common for SPA routes)
|
|
120
|
+
index_in_dir = base_dir / path_normalized / 'index.html'
|
|
121
|
+
if index_in_dir.exists():
|
|
122
|
+
resolved_path = f"{path_normalized}/index.html"
|
|
123
|
+
logger.debug(f"[SPA Router] Resolved {path} → {resolved_path}")
|
|
124
|
+
return resolved_path
|
|
125
|
+
|
|
126
|
+
# Strategy 3: Try with trailing slash + index.html
|
|
127
|
+
if path.endswith('/'):
|
|
128
|
+
index_path = path + 'index.html'
|
|
129
|
+
if (base_dir / index_path).exists():
|
|
130
|
+
logger.debug(f"[SPA Router] Trailing slash resolved: {index_path}")
|
|
131
|
+
return index_path
|
|
132
|
+
|
|
133
|
+
# Strategy 4: Try path.html (Next.js static export behavior)
|
|
134
|
+
html_file = base_dir / (path_normalized + '.html')
|
|
135
|
+
if html_file.exists():
|
|
136
|
+
resolved_path = path_normalized + '.html'
|
|
137
|
+
logger.debug(f"[SPA Router] HTML file match: {resolved_path}")
|
|
138
|
+
return resolved_path
|
|
139
|
+
|
|
140
|
+
# Strategy 5: Check if it's a directory without index.html
|
|
141
|
+
if file_path.exists() and file_path.is_dir():
|
|
142
|
+
# Try index.html in that directory
|
|
143
|
+
index_in_existing_dir = file_path / 'index.html'
|
|
144
|
+
if index_in_existing_dir.exists():
|
|
145
|
+
resolved_path = f"{path_normalized}/index.html"
|
|
146
|
+
logger.debug(f"[SPA Router] Directory with index: {resolved_path}")
|
|
147
|
+
return resolved_path
|
|
148
|
+
|
|
149
|
+
# Strategy 6: SPA fallback - serve root index.html
|
|
150
|
+
# This allows client-side routing to handle unknown routes
|
|
151
|
+
root_index = base_dir / 'index.html'
|
|
152
|
+
if root_index.exists():
|
|
153
|
+
logger.debug(f"[SPA Router] Fallback to index.html for route: {path}")
|
|
154
|
+
return 'index.html'
|
|
155
|
+
|
|
156
|
+
# Strategy 7: Nothing found - return original path (will 404)
|
|
157
|
+
logger.warning(f"[SPA Router] No match found for: {path}")
|
|
158
|
+
return path
|
|
159
|
+
|
|
119
160
|
def _should_inject_jwt(self, request, response):
|
|
120
161
|
"""Check if JWT tokens should be injected."""
|
|
121
162
|
# Only for authenticated users
|
django_cfg/apps/urls.py
CHANGED
|
@@ -183,66 +183,8 @@ class InstalledAppsBuilder:
|
|
|
183
183
|
# django-browser-reload not installed, skip it
|
|
184
184
|
pass
|
|
185
185
|
|
|
186
|
-
# Auto-detect dashboard apps from Unfold callback
|
|
187
|
-
dashboard_apps = self._get_dashboard_apps_from_callback()
|
|
188
|
-
apps.extend(dashboard_apps)
|
|
189
|
-
|
|
190
186
|
return apps
|
|
191
187
|
|
|
192
|
-
def _get_dashboard_apps_from_callback(self) -> List[str]:
|
|
193
|
-
"""
|
|
194
|
-
Auto-detect dashboard apps from Unfold dashboard_callback setting.
|
|
195
|
-
|
|
196
|
-
Extracts app names from callback paths like:
|
|
197
|
-
- "api.dashboard.callbacks.main_dashboard_callback" → ["api.dashboard"]
|
|
198
|
-
- "myproject.admin.callbacks.dashboard" → ["myproject.admin"]
|
|
199
|
-
|
|
200
|
-
This allows django-cfg to automatically add dashboard apps to INSTALLED_APPS
|
|
201
|
-
without requiring manual configuration.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
List of dashboard app names to add to INSTALLED_APPS
|
|
205
|
-
"""
|
|
206
|
-
dashboard_apps = []
|
|
207
|
-
|
|
208
|
-
# Check if Unfold is configured with a theme
|
|
209
|
-
if not self.config.unfold or not self.config.unfold.theme:
|
|
210
|
-
return dashboard_apps
|
|
211
|
-
|
|
212
|
-
# Get dashboard callback path from theme
|
|
213
|
-
callback_path = getattr(self.config.unfold.theme, "dashboard_callback", None)
|
|
214
|
-
if not callback_path:
|
|
215
|
-
return dashboard_apps
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
# Parse callback path: "api.dashboard.callbacks.main_dashboard_callback"
|
|
219
|
-
# Extract app part: "api.dashboard"
|
|
220
|
-
parts = callback_path.split(".")
|
|
221
|
-
|
|
222
|
-
# Look for common callback patterns
|
|
223
|
-
callback_indicators = ["callbacks", "views", "handlers"]
|
|
224
|
-
|
|
225
|
-
# Find the callback indicator and extract app path before it
|
|
226
|
-
app_parts = []
|
|
227
|
-
for i, part in enumerate(parts):
|
|
228
|
-
if part in callback_indicators:
|
|
229
|
-
app_parts = parts[:i] # Everything before the callback indicator
|
|
230
|
-
break
|
|
231
|
-
|
|
232
|
-
# If no callback indicator found, assume last part is function name
|
|
233
|
-
if not app_parts and len(parts) > 1:
|
|
234
|
-
app_parts = parts[:-1] # Everything except the last part
|
|
235
|
-
|
|
236
|
-
if app_parts:
|
|
237
|
-
app_name = ".".join(app_parts)
|
|
238
|
-
dashboard_apps.append(app_name)
|
|
239
|
-
|
|
240
|
-
except Exception:
|
|
241
|
-
# If parsing fails, silently continue - dashboard callback is optional
|
|
242
|
-
pass
|
|
243
|
-
|
|
244
|
-
return dashboard_apps
|
|
245
|
-
|
|
246
188
|
def _deduplicate(self, apps: List[str]) -> List[str]:
|
|
247
189
|
"""
|
|
248
190
|
Remove duplicate apps while preserving order.
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Django CFG Unfold Module
|
|
3
3
|
|
|
4
|
-
Provides complete Unfold admin interface integration with
|
|
5
|
-
navigation
|
|
4
|
+
Provides complete Unfold admin interface integration with
|
|
5
|
+
navigation and theming support.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .callbacks import UnfoldCallbacks
|
|
9
|
-
from .dashboard import DashboardManager, get_dashboard_manager
|
|
10
8
|
from .models import *
|
|
9
|
+
from .navigation import NavigationManager, get_navigation_manager
|
|
11
10
|
from .system_monitor import SystemMonitor
|
|
12
11
|
from .tailwind import get_css_variables, get_unfold_colors
|
|
13
12
|
|
|
@@ -20,19 +19,10 @@ def get_system_monitor() -> SystemMonitor:
|
|
|
20
19
|
globals()['_system_monitor'] = SystemMonitor()
|
|
21
20
|
return globals()['_system_monitor']
|
|
22
21
|
|
|
23
|
-
def get_unfold_callbacks() -> UnfoldCallbacks:
|
|
24
|
-
"""Get the global unfold callbacks instance."""
|
|
25
|
-
global _unfold_callbacks
|
|
26
|
-
if '_unfold_callbacks' not in globals():
|
|
27
|
-
globals()['_unfold_callbacks'] = UnfoldCallbacks()
|
|
28
|
-
return globals()['_unfold_callbacks']
|
|
29
|
-
|
|
30
22
|
# Export main components
|
|
31
23
|
__all__ = [
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'UnfoldCallbacks',
|
|
35
|
-
'get_unfold_callbacks',
|
|
24
|
+
'NavigationManager',
|
|
25
|
+
'get_navigation_manager',
|
|
36
26
|
'SystemMonitor',
|
|
37
27
|
'get_system_monitor',
|
|
38
28
|
'get_unfold_colors',
|
|
@@ -48,15 +38,6 @@ __all__ = [
|
|
|
48
38
|
'NavigationSection',
|
|
49
39
|
'NavigationItemType',
|
|
50
40
|
'SiteDropdownItem',
|
|
51
|
-
'StatCard',
|
|
52
|
-
'SystemHealthItem',
|
|
53
|
-
'QuickAction',
|
|
54
|
-
'DashboardWidget',
|
|
55
|
-
'DashboardData',
|
|
56
|
-
'ChartDataset',
|
|
57
|
-
'ChartData',
|
|
58
|
-
'TabConfiguration',
|
|
59
|
-
'TabItem',
|
|
60
41
|
]
|
|
61
42
|
|
|
62
43
|
# Version info
|
|
@@ -12,18 +12,8 @@ from .config import (
|
|
|
12
12
|
UnfoldTheme,
|
|
13
13
|
UnfoldThemeConfig,
|
|
14
14
|
)
|
|
15
|
-
from .dashboard import (
|
|
16
|
-
ChartData,
|
|
17
|
-
ChartDataset,
|
|
18
|
-
DashboardData,
|
|
19
|
-
DashboardWidget,
|
|
20
|
-
QuickAction,
|
|
21
|
-
StatCard,
|
|
22
|
-
SystemHealthItem,
|
|
23
|
-
)
|
|
24
15
|
from .dropdown import SiteDropdownItem
|
|
25
16
|
from .navigation import NavigationItem, NavigationItemType, NavigationSection
|
|
26
|
-
from .tabs import TabConfiguration, TabItem
|
|
27
17
|
|
|
28
18
|
__all__ = [
|
|
29
19
|
# Config models
|
|
@@ -41,17 +31,4 @@ __all__ = [
|
|
|
41
31
|
|
|
42
32
|
# Dropdown models
|
|
43
33
|
'SiteDropdownItem',
|
|
44
|
-
|
|
45
|
-
# Dashboard models
|
|
46
|
-
'StatCard',
|
|
47
|
-
'SystemHealthItem',
|
|
48
|
-
'QuickAction',
|
|
49
|
-
'DashboardWidget',
|
|
50
|
-
'DashboardData',
|
|
51
|
-
'ChartDataset',
|
|
52
|
-
'ChartData',
|
|
53
|
-
|
|
54
|
-
# Tab models
|
|
55
|
-
'TabConfiguration',
|
|
56
|
-
'TabItem',
|
|
57
34
|
]
|