django-cfg 1.4.84__py3-none-any.whl → 1.4.85__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/dashboard/serializers/__init__.py +55 -0
- django_cfg/apps/dashboard/serializers/activity.py +38 -0
- django_cfg/apps/dashboard/serializers/apizones.py +26 -0
- django_cfg/apps/dashboard/serializers/base.py +16 -0
- django_cfg/apps/dashboard/serializers/charts.py +44 -0
- django_cfg/apps/dashboard/serializers/commands.py +26 -0
- django_cfg/apps/dashboard/serializers/overview.py +34 -0
- django_cfg/apps/dashboard/serializers/statistics.py +46 -0
- django_cfg/apps/dashboard/serializers/system.py +58 -0
- django_cfg/apps/dashboard/services/__init__.py +10 -1
- django_cfg/apps/dashboard/services/apizones_service.py +119 -0
- django_cfg/apps/dashboard/services/charts_service.py +266 -0
- django_cfg/apps/dashboard/services/commands_service.py +142 -0
- django_cfg/apps/dashboard/services/statistics_service.py +262 -104
- django_cfg/apps/dashboard/urls.py +25 -6
- django_cfg/apps/dashboard/views/__init__.py +23 -0
- django_cfg/apps/dashboard/views/activity_views.py +83 -0
- django_cfg/apps/dashboard/views/apizones_views.py +73 -0
- django_cfg/apps/dashboard/views/charts_views.py +159 -0
- django_cfg/apps/dashboard/views/commands_views.py +73 -0
- django_cfg/apps/dashboard/views/overview_views.py +92 -0
- django_cfg/apps/dashboard/views/statistics_views.py +105 -0
- django_cfg/apps/dashboard/views/system_views.py +73 -0
- django_cfg/modules/django_unfold/callbacks/main.py +7 -6
- django_cfg/modules/django_unfold/dashboard.py +1 -36
- django_cfg/modules/django_unfold/models/config.py +102 -73
- django_cfg/modules/django_unfold/tailwind.py +31 -79
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/frontend/admin/404.html +1 -1
- django_cfg/static/frontend/admin/500.html +1 -1
- django_cfg/static/frontend/admin/_next/static/BembwiEtlu4eFl3OX7n1k/_buildManifest.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/23004-faae121bbfecc163.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{20695.a7d37b6c40ad3f58.js → 43076.55dd23b6cd68edb0.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-3b9d15242191c8bc.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{64330.2ef79bccd7d4e363.js → 64330.41858e98c0e5173b.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/6766.8d01e44e83070e83.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{96168.eb7fdb721b9cdb00.js → 96168.b7197f890097df6e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{404-c283223d1afd02a2.js → 404-cf71cd7b3cb005e5.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{500-389d6d3e1f2f7fda.js → 500-ff19c7842e3df415.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-f62e5528fbcbb6b3.js +272 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{_error-5291033275c26d09.js → _error-87f3fdc2aa131e77.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{index-d7bc30185f52cbca.js → index-69f737d4802cc5b7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-f24beb6ed3955aa8.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{profile-e93a65e8e7d9022b.js → profile-b8045f993287f1a7.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{ui-669e8f2a785beba2.js → ui-373fff8b42878e64.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-fe9faa86ecdb0ce6.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/{webpack-92add5f95c66e349.js → webpack-7c456a65e96eb97e.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/css/5f9a37b6e6a72303.css +3 -0
- django_cfg/static/frontend/admin/auth.html +1 -1
- django_cfg/static/frontend/admin/index.html +1 -1
- django_cfg/static/frontend/admin/legal/cookies.html +1 -1
- django_cfg/static/frontend/admin/legal/privacy.html +1 -1
- django_cfg/static/frontend/admin/legal/security.html +1 -1
- django_cfg/static/frontend/admin/legal/terms.html +1 -1
- django_cfg/static/frontend/admin/private/centrifugo.html +1 -1
- django_cfg/static/frontend/admin/private/profile.html +1 -1
- django_cfg/static/frontend/admin/private/ui.html +1 -1
- django_cfg/static/frontend/admin/private.html +1 -1
- django_cfg/templates/admin/index.html +237 -5
- django_cfg/templates/admin/sections/commands_section.html +5 -0
- django_cfg/templates/admin/sections/documentation_section.html +5 -0
- django_cfg/templates/admin/sections/overview_section.html +5 -0
- django_cfg/templates/admin/sections/stats_section.html +5 -0
- django_cfg/templates/admin/sections/system_section.html +5 -0
- django_cfg/templates/admin/sections/widgets_section.html +11 -0
- django_cfg/templates/unfold/layouts/skeleton.html +27 -0
- django_cfg/templatetags/django_cfg.py +53 -0
- {django_cfg-1.4.84.dist-info → django_cfg-1.4.85.dist-info}/METADATA +1 -1
- {django_cfg-1.4.84.dist-info → django_cfg-1.4.85.dist-info}/RECORD +74 -51
- django_cfg/apps/dashboard/api/__init__.py +0 -27
- django_cfg/apps/dashboard/api/serializers.py +0 -165
- django_cfg/apps/dashboard/api/viewsets.py +0 -257
- django_cfg/static/frontend/admin/_next/static/-Zk0eDB7OJOEFrFyR5BwZ/_buildManifest.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/43076-4be6a9794e9c3e8b.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-79c02212788f1ec7.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/6766.d62fed7cd4761148.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/82296-a2c8d38f62224be5.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/_app-f25bec36bbdc9625.js +0 -272
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/centrifugo-22532c65971225eb.js +0 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private-a8a9ba76f2c75354.js +0 -1
- django_cfg/static/frontend/admin/_next/static/css/78d677ac1677c210.css +0 -3
- /django_cfg/static/frontend/admin/_next/static/{-Zk0eDB7OJOEFrFyR5BwZ → BembwiEtlu4eFl3OX7n1k}/_ssgManifest.js +0 -0
- {django_cfg-1.4.84.dist-info → django_cfg-1.4.85.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.84.dist-info → django_cfg-1.4.85.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.84.dist-info → django_cfg-1.4.85.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
Statistics Service
|
|
3
3
|
|
|
4
4
|
Collects and aggregates statistics for dashboard display.
|
|
5
|
-
|
|
5
|
+
Uses Django ORM for real data collection.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
from datetime import datetime, timedelta
|
|
10
10
|
from typing import Any, Dict, List, Optional
|
|
11
11
|
|
|
12
|
+
from django.apps import apps
|
|
13
|
+
from django.contrib.auth import get_user_model
|
|
14
|
+
from django.utils import timezone
|
|
15
|
+
|
|
12
16
|
logger = logging.getLogger(__name__)
|
|
13
17
|
|
|
14
18
|
|
|
@@ -17,7 +21,7 @@ class StatisticsService:
|
|
|
17
21
|
Service for collecting dashboard statistics.
|
|
18
22
|
|
|
19
23
|
%%PRIORITY:HIGH%%
|
|
20
|
-
%%AI_HINT: This service collects data from various
|
|
24
|
+
%%AI_HINT: This service collects data from various Django models using ORM%%
|
|
21
25
|
|
|
22
26
|
TAGS: statistics, dashboard, service
|
|
23
27
|
"""
|
|
@@ -26,6 +30,10 @@ class StatisticsService:
|
|
|
26
30
|
"""Initialize statistics service."""
|
|
27
31
|
self.logger = logger
|
|
28
32
|
|
|
33
|
+
def _get_user_model(self):
|
|
34
|
+
"""Get the user model safely."""
|
|
35
|
+
return get_user_model()
|
|
36
|
+
|
|
29
37
|
def get_user_statistics(self) -> Dict[str, Any]:
|
|
30
38
|
"""
|
|
31
39
|
Get user-related statistics.
|
|
@@ -33,23 +41,27 @@ class StatisticsService:
|
|
|
33
41
|
Returns:
|
|
34
42
|
Dictionary containing user stats:
|
|
35
43
|
- total_users: Total number of users
|
|
36
|
-
- active_users: Number of active users
|
|
44
|
+
- active_users: Number of active users
|
|
37
45
|
- new_users: New users in last 7 days
|
|
38
46
|
- superusers: Number of superusers
|
|
39
47
|
|
|
40
|
-
%%AI_HINT:
|
|
48
|
+
%%AI_HINT: Uses real Django ORM queries from User model%%
|
|
41
49
|
"""
|
|
42
50
|
try:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
User = self._get_user_model()
|
|
52
|
+
|
|
53
|
+
total_users = User.objects.count()
|
|
54
|
+
active_users = User.objects.filter(is_active=True).count()
|
|
55
|
+
new_users_7d = User.objects.filter(
|
|
56
|
+
date_joined__gte=timezone.now() - timedelta(days=7)
|
|
57
|
+
).count()
|
|
58
|
+
superusers = User.objects.filter(is_superuser=True).count()
|
|
47
59
|
|
|
48
60
|
return {
|
|
49
|
-
'total_users':
|
|
50
|
-
'active_users':
|
|
51
|
-
'new_users':
|
|
52
|
-
'superusers':
|
|
61
|
+
'total_users': total_users,
|
|
62
|
+
'active_users': active_users,
|
|
63
|
+
'new_users': new_users_7d,
|
|
64
|
+
'superusers': superusers,
|
|
53
65
|
}
|
|
54
66
|
except Exception as e:
|
|
55
67
|
self.logger.error(f"Error getting user statistics: {e}")
|
|
@@ -61,48 +73,93 @@ class StatisticsService:
|
|
|
61
73
|
'error': str(e)
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
def get_app_statistics(self) -> Dict[str,
|
|
76
|
+
def get_app_statistics(self) -> Dict[str, Any]:
|
|
65
77
|
"""
|
|
66
78
|
Get application-specific statistics.
|
|
67
79
|
|
|
68
80
|
Returns:
|
|
69
|
-
Dictionary with app
|
|
70
|
-
Example:
|
|
81
|
+
Dictionary with aggregated app statistics.
|
|
71
82
|
{
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
|
|
83
|
+
'apps': {app_label: {...stats...}},
|
|
84
|
+
'total_records': int,
|
|
85
|
+
'total_models': int,
|
|
86
|
+
'total_apps': int
|
|
75
87
|
}
|
|
76
88
|
|
|
77
|
-
%%AI_HINT:
|
|
89
|
+
%%AI_HINT: Real app introspection using Django apps registry%%
|
|
78
90
|
"""
|
|
79
91
|
try:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'subscribers': 2500,
|
|
99
|
-
'campaigns': 12,
|
|
100
|
-
'sent': 28000,
|
|
101
|
-
}
|
|
102
|
-
}
|
|
92
|
+
stats = {"apps": {}, "total_records": 0, "total_models": 0, "total_apps": 0}
|
|
93
|
+
|
|
94
|
+
# Get all installed apps
|
|
95
|
+
for app_config in apps.get_app_configs():
|
|
96
|
+
app_label = app_config.label
|
|
97
|
+
|
|
98
|
+
# Skip system apps
|
|
99
|
+
if app_label in ["admin", "contenttypes", "sessions", "auth"]:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
app_stats = self._get_app_stats(app_label)
|
|
103
|
+
if app_stats:
|
|
104
|
+
stats["apps"][app_label] = app_stats
|
|
105
|
+
stats["total_records"] += app_stats.get("total_records", 0)
|
|
106
|
+
stats["total_models"] += app_stats.get("model_count", 0)
|
|
107
|
+
stats["total_apps"] += 1
|
|
108
|
+
|
|
109
|
+
return stats
|
|
103
110
|
except Exception as e:
|
|
104
111
|
self.logger.error(f"Error getting app statistics: {e}")
|
|
105
|
-
return {'error': str(e)}
|
|
112
|
+
return {'apps': {}, 'total_records': 0, 'total_models': 0, 'total_apps': 0, 'error': str(e)}
|
|
113
|
+
|
|
114
|
+
def _get_app_stats(self, app_label: str) -> Optional[Dict[str, Any]]:
|
|
115
|
+
"""Get statistics for a specific app."""
|
|
116
|
+
try:
|
|
117
|
+
app_config = apps.get_app_config(app_label)
|
|
118
|
+
# Convert generator to list to avoid len() error
|
|
119
|
+
models_list = list(app_config.get_models())
|
|
120
|
+
|
|
121
|
+
if not models_list:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
app_stats = {
|
|
125
|
+
"name": app_config.verbose_name or app_label.title(),
|
|
126
|
+
"models": {},
|
|
127
|
+
"total_records": 0,
|
|
128
|
+
"model_count": len(models_list),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for model in models_list:
|
|
132
|
+
try:
|
|
133
|
+
# Get model statistics
|
|
134
|
+
model_stats = self._get_model_stats(model)
|
|
135
|
+
if model_stats:
|
|
136
|
+
app_stats["models"][model._meta.model_name] = model_stats
|
|
137
|
+
app_stats["total_records"] += model_stats.get("count", 0)
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
return app_stats
|
|
142
|
+
|
|
143
|
+
except Exception:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def _get_model_stats(self, model) -> Optional[Dict[str, Any]]:
|
|
147
|
+
"""Get statistics for a specific model."""
|
|
148
|
+
try:
|
|
149
|
+
# Get basic model info
|
|
150
|
+
model_stats = {
|
|
151
|
+
"name": model._meta.verbose_name_plural
|
|
152
|
+
or model._meta.verbose_name
|
|
153
|
+
or model._meta.model_name,
|
|
154
|
+
"count": model.objects.count(),
|
|
155
|
+
"fields_count": len(model._meta.fields),
|
|
156
|
+
"admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return model_stats
|
|
160
|
+
|
|
161
|
+
except Exception:
|
|
162
|
+
return None
|
|
106
163
|
|
|
107
164
|
def get_stat_cards(self) -> List[Dict[str, Any]]:
|
|
108
165
|
"""
|
|
@@ -113,42 +170,59 @@ class StatisticsService:
|
|
|
113
170
|
Each card contains: title, value, icon, change, change_type
|
|
114
171
|
|
|
115
172
|
USED_BY: DashboardViewSet.overview endpoint
|
|
173
|
+
%%AI_HINT: Real data from User model with calculated changes%%
|
|
116
174
|
"""
|
|
117
175
|
try:
|
|
118
176
|
user_stats = self.get_user_statistics()
|
|
119
177
|
|
|
178
|
+
total_users = user_stats['total_users']
|
|
179
|
+
active_users = user_stats['active_users']
|
|
180
|
+
new_users_7d = user_stats['new_users']
|
|
181
|
+
superusers = user_stats['superusers']
|
|
182
|
+
|
|
120
183
|
cards = [
|
|
121
184
|
{
|
|
122
185
|
'title': 'Total Users',
|
|
123
|
-
'value':
|
|
186
|
+
'value': f"{total_users:,}",
|
|
124
187
|
'icon': 'people',
|
|
125
|
-
'change':
|
|
126
|
-
'change_type': 'positive',
|
|
188
|
+
'change': f"+{new_users_7d}" if new_users_7d > 0 else None,
|
|
189
|
+
'change_type': 'positive' if new_users_7d > 0 else 'neutral',
|
|
127
190
|
'color': 'primary',
|
|
191
|
+
'description': 'Registered users',
|
|
128
192
|
},
|
|
129
193
|
{
|
|
130
194
|
'title': 'Active Users',
|
|
131
|
-
'value':
|
|
195
|
+
'value': f"{active_users:,}",
|
|
132
196
|
'icon': 'person',
|
|
133
|
-
'change':
|
|
134
|
-
|
|
197
|
+
'change': (
|
|
198
|
+
f"{(active_users/total_users*100):.1f}%"
|
|
199
|
+
if total_users > 0
|
|
200
|
+
else "0%"
|
|
201
|
+
),
|
|
202
|
+
'change_type': (
|
|
203
|
+
'positive' if active_users > total_users * 0.7 else 'neutral'
|
|
204
|
+
),
|
|
135
205
|
'color': 'success',
|
|
206
|
+
'description': 'Currently active',
|
|
136
207
|
},
|
|
137
208
|
{
|
|
138
|
-
'title': 'New
|
|
139
|
-
'value':
|
|
209
|
+
'title': 'New This Week',
|
|
210
|
+
'value': f"{new_users_7d:,}",
|
|
140
211
|
'icon': 'person_add',
|
|
141
|
-
'
|
|
142
|
-
'
|
|
143
|
-
'
|
|
212
|
+
'change_type': 'positive' if new_users_7d > 0 else 'neutral',
|
|
213
|
+
'color': 'info',
|
|
214
|
+
'description': 'Last 7 days',
|
|
144
215
|
},
|
|
145
216
|
{
|
|
146
|
-
'title': '
|
|
147
|
-
'value':
|
|
148
|
-
'icon': '
|
|
149
|
-
'change':
|
|
150
|
-
|
|
151
|
-
|
|
217
|
+
'title': 'Superusers',
|
|
218
|
+
'value': f"{superusers:,}",
|
|
219
|
+
'icon': 'admin_panel_settings',
|
|
220
|
+
'change': (
|
|
221
|
+
f"{(superusers/total_users*100):.1f}%" if total_users > 0 else "0%"
|
|
222
|
+
),
|
|
223
|
+
'change_type': 'neutral',
|
|
224
|
+
'color': 'warning',
|
|
225
|
+
'description': 'Administrative access',
|
|
152
226
|
},
|
|
153
227
|
]
|
|
154
228
|
|
|
@@ -168,48 +242,96 @@ class StatisticsService:
|
|
|
168
242
|
Returns:
|
|
169
243
|
List of recent activity entries
|
|
170
244
|
|
|
171
|
-
%%AI_HINT:
|
|
245
|
+
%%AI_HINT: Returns placeholder - connect to django-auditlog when available%%
|
|
172
246
|
"""
|
|
173
247
|
try:
|
|
174
|
-
#
|
|
175
|
-
|
|
248
|
+
# Note: This is a placeholder. Real implementation should connect to:
|
|
249
|
+
# - django-auditlog (if installed)
|
|
250
|
+
# - Custom activity/history tracking
|
|
251
|
+
# - Django admin logs (LogEntry model)
|
|
176
252
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
'resource': 'Lead #1234',
|
|
183
|
-
'timestamp': (now - timedelta(minutes=5)).isoformat(),
|
|
184
|
-
'icon': 'add_circle',
|
|
185
|
-
'color': 'success',
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
'id': 2,
|
|
189
|
-
'user': 'admin@example.com',
|
|
190
|
-
'action': 'updated',
|
|
191
|
-
'resource': 'User Settings',
|
|
192
|
-
'timestamp': (now - timedelta(minutes=15)).isoformat(),
|
|
193
|
-
'icon': 'edit',
|
|
194
|
-
'color': 'primary',
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
'id': 3,
|
|
198
|
-
'user': 'sarah@example.com',
|
|
199
|
-
'action': 'completed',
|
|
200
|
-
'resource': 'Task #456',
|
|
201
|
-
'timestamp': (now - timedelta(hours=1)).isoformat(),
|
|
202
|
-
'icon': 'check_circle',
|
|
203
|
-
'color': 'success',
|
|
204
|
-
},
|
|
205
|
-
]
|
|
253
|
+
# Try to get Django admin logs as fallback
|
|
254
|
+
from django.contrib.admin.models import LogEntry
|
|
255
|
+
|
|
256
|
+
activities = []
|
|
257
|
+
log_entries = LogEntry.objects.select_related('user', 'content_type').order_by('-action_time')[:limit]
|
|
206
258
|
|
|
207
|
-
|
|
259
|
+
for entry in log_entries:
|
|
260
|
+
activities.append({
|
|
261
|
+
'id': entry.pk,
|
|
262
|
+
'user': entry.user.get_username() if entry.user else 'System',
|
|
263
|
+
'action': entry.get_action_flag_display().lower(),
|
|
264
|
+
'resource': str(entry),
|
|
265
|
+
'timestamp': entry.action_time.isoformat(),
|
|
266
|
+
'icon': self._get_activity_icon(entry.action_flag),
|
|
267
|
+
'color': self._get_activity_color(entry.action_flag),
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return activities
|
|
208
271
|
|
|
209
272
|
except Exception as e:
|
|
210
273
|
self.logger.error(f"Error getting recent activity: {e}")
|
|
211
274
|
return []
|
|
212
275
|
|
|
276
|
+
def _get_activity_icon(self, action_flag: int) -> str:
|
|
277
|
+
"""Get icon for activity based on action flag."""
|
|
278
|
+
icons = {
|
|
279
|
+
1: 'add_circle', # ADDITION
|
|
280
|
+
2: 'edit', # CHANGE
|
|
281
|
+
3: 'delete', # DELETION
|
|
282
|
+
}
|
|
283
|
+
return icons.get(action_flag, 'circle')
|
|
284
|
+
|
|
285
|
+
def _get_activity_color(self, action_flag: int) -> str:
|
|
286
|
+
"""Get color for activity based on action flag."""
|
|
287
|
+
colors = {
|
|
288
|
+
1: 'success', # ADDITION
|
|
289
|
+
2: 'primary', # CHANGE
|
|
290
|
+
3: 'error', # DELETION
|
|
291
|
+
}
|
|
292
|
+
return colors.get(action_flag, 'default')
|
|
293
|
+
|
|
294
|
+
def get_recent_users(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
295
|
+
"""
|
|
296
|
+
Get recent users data.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
limit: Maximum number of users to return
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
List of recent user dictionaries with admin URLs
|
|
303
|
+
|
|
304
|
+
%%AI_HINT: Real data from User model with admin URLs%%
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
User = self._get_user_model()
|
|
308
|
+
recent_users = User.objects.select_related().order_by("-date_joined")[:limit]
|
|
309
|
+
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
"id": user.id,
|
|
313
|
+
"username": user.username,
|
|
314
|
+
"email": user.email or "No email",
|
|
315
|
+
"date_joined": (
|
|
316
|
+
user.date_joined.strftime("%Y-%m-%d %H:%M")
|
|
317
|
+
if user.date_joined
|
|
318
|
+
else "Unknown"
|
|
319
|
+
),
|
|
320
|
+
"is_active": user.is_active,
|
|
321
|
+
"is_staff": user.is_staff,
|
|
322
|
+
"is_superuser": user.is_superuser,
|
|
323
|
+
"last_login": (
|
|
324
|
+
user.last_login.strftime("%Y-%m-%d %H:%M")
|
|
325
|
+
if user.last_login
|
|
326
|
+
else None
|
|
327
|
+
),
|
|
328
|
+
}
|
|
329
|
+
for user in recent_users
|
|
330
|
+
]
|
|
331
|
+
except Exception as e:
|
|
332
|
+
self.logger.error(f"Error getting recent users: {e}")
|
|
333
|
+
return []
|
|
334
|
+
|
|
213
335
|
def get_system_metrics(self) -> Dict[str, Any]:
|
|
214
336
|
"""
|
|
215
337
|
Get system performance metrics.
|
|
@@ -217,19 +339,55 @@ class StatisticsService:
|
|
|
217
339
|
Returns:
|
|
218
340
|
Dictionary with system metrics (CPU, memory, disk, etc.)
|
|
219
341
|
|
|
220
|
-
%%AI_HINT:
|
|
342
|
+
%%AI_HINT: Uses psutil for real system metrics when available%%
|
|
221
343
|
"""
|
|
222
344
|
try:
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
345
|
+
# Try to use psutil for real system metrics
|
|
346
|
+
try:
|
|
347
|
+
import psutil
|
|
348
|
+
import time
|
|
349
|
+
|
|
350
|
+
# Get CPU usage
|
|
351
|
+
cpu_percent = psutil.cpu_percent(interval=0.1)
|
|
352
|
+
|
|
353
|
+
# Get memory usage
|
|
354
|
+
memory = psutil.virtual_memory()
|
|
355
|
+
memory_percent = memory.percent
|
|
356
|
+
|
|
357
|
+
# Get disk usage for root partition
|
|
358
|
+
disk = psutil.disk_usage('/')
|
|
359
|
+
disk_percent = disk.percent
|
|
360
|
+
|
|
361
|
+
# Get network IO
|
|
362
|
+
net_io = psutil.net_io_counters()
|
|
363
|
+
|
|
364
|
+
# Get system uptime
|
|
365
|
+
boot_time = psutil.boot_time()
|
|
366
|
+
uptime_seconds = time.time() - boot_time
|
|
367
|
+
uptime_days = int(uptime_seconds // 86400)
|
|
368
|
+
uptime_hours = int((uptime_seconds % 86400) // 3600)
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
'cpu_usage': round(cpu_percent, 1),
|
|
372
|
+
'memory_usage': round(memory_percent, 1),
|
|
373
|
+
'disk_usage': round(disk_percent, 1),
|
|
374
|
+
'network_in': f"{net_io.bytes_recv / (1024**2):.1f} MB",
|
|
375
|
+
'network_out': f"{net_io.bytes_sent / (1024**2):.1f} MB",
|
|
376
|
+
'response_time': '< 100ms', # Placeholder - can be calculated from middleware
|
|
377
|
+
'uptime': f"{uptime_days} days, {uptime_hours} hours",
|
|
378
|
+
}
|
|
379
|
+
except ImportError:
|
|
380
|
+
self.logger.warning("psutil not installed, returning placeholder metrics")
|
|
381
|
+
# Fallback to placeholder data if psutil not available
|
|
382
|
+
return {
|
|
383
|
+
'cpu_usage': 0,
|
|
384
|
+
'memory_usage': 0,
|
|
385
|
+
'disk_usage': 0,
|
|
386
|
+
'network_in': 'N/A',
|
|
387
|
+
'network_out': 'N/A',
|
|
388
|
+
'response_time': 'N/A',
|
|
389
|
+
'uptime': 'N/A',
|
|
390
|
+
}
|
|
233
391
|
except Exception as e:
|
|
234
392
|
self.logger.error(f"Error getting system metrics: {e}")
|
|
235
393
|
return {'error': str(e)}
|
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Dashboard URLs
|
|
3
3
|
|
|
4
|
-
RESTful API endpoints for dashboard data.
|
|
5
|
-
|
|
4
|
+
RESTful API endpoints for dashboard data organized by domain.
|
|
5
|
+
|
|
6
|
+
API Structure:
|
|
7
|
+
- /api/overview/ - Complete dashboard overview
|
|
8
|
+
- /api/statistics/ - Statistics endpoints (cards, users, apps)
|
|
9
|
+
- /api/system/ - System monitoring (health, metrics)
|
|
10
|
+
- /api/activity/ - Activity tracking (recent, actions)
|
|
6
11
|
"""
|
|
7
12
|
|
|
8
13
|
from django.urls import include, path
|
|
9
14
|
from rest_framework.routers import DefaultRouter
|
|
10
15
|
|
|
11
|
-
from .
|
|
16
|
+
from .views import (
|
|
17
|
+
OverviewViewSet,
|
|
18
|
+
StatisticsViewSet,
|
|
19
|
+
SystemViewSet,
|
|
20
|
+
ActivityViewSet,
|
|
21
|
+
ChartsViewSet,
|
|
22
|
+
CommandsViewSet,
|
|
23
|
+
APIZonesViewSet,
|
|
24
|
+
)
|
|
12
25
|
|
|
13
|
-
app_name = '
|
|
26
|
+
app_name = 'django_cfg_dashboard'
|
|
14
27
|
|
|
15
28
|
# Main router for ViewSets
|
|
16
29
|
router = DefaultRouter()
|
|
17
|
-
router.register(r'',
|
|
30
|
+
router.register(r'overview', OverviewViewSet, basename='overview')
|
|
31
|
+
router.register(r'statistics', StatisticsViewSet, basename='statistics')
|
|
32
|
+
router.register(r'system', SystemViewSet, basename='system')
|
|
33
|
+
router.register(r'activity', ActivityViewSet, basename='activity')
|
|
34
|
+
router.register(r'charts', ChartsViewSet, basename='charts')
|
|
35
|
+
router.register(r'commands', CommandsViewSet, basename='commands')
|
|
36
|
+
router.register(r'zones', APIZonesViewSet, basename='zones')
|
|
18
37
|
|
|
19
38
|
urlpatterns = [
|
|
20
39
|
# RESTful API endpoints using ViewSets
|
|
21
|
-
# Mounted at cfg/dashboard/api/
|
|
40
|
+
# Mounted at /cfg/dashboard/api/
|
|
22
41
|
path('api/', include(router.urls)),
|
|
23
42
|
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dashboard Views
|
|
3
|
+
|
|
4
|
+
ViewSets organized by domain for better maintainability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .overview_views import OverviewViewSet
|
|
8
|
+
from .statistics_views import StatisticsViewSet
|
|
9
|
+
from .system_views import SystemViewSet
|
|
10
|
+
from .activity_views import ActivityViewSet
|
|
11
|
+
from .charts_views import ChartsViewSet
|
|
12
|
+
from .commands_views import CommandsViewSet
|
|
13
|
+
from .apizones_views import APIZonesViewSet
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'OverviewViewSet',
|
|
17
|
+
'StatisticsViewSet',
|
|
18
|
+
'SystemViewSet',
|
|
19
|
+
'ActivityViewSet',
|
|
20
|
+
'ChartsViewSet',
|
|
21
|
+
'CommandsViewSet',
|
|
22
|
+
'APIZonesViewSet',
|
|
23
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Activity ViewSet
|
|
3
|
+
|
|
4
|
+
Endpoints for activity tracking:
|
|
5
|
+
- GET /activity/recent/ - Recent activity entries
|
|
6
|
+
- GET /activity/actions/ - Quick action buttons
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
12
|
+
from rest_framework import status, viewsets
|
|
13
|
+
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
|
|
14
|
+
from rest_framework.decorators import action
|
|
15
|
+
from rest_framework.permissions import IsAdminUser
|
|
16
|
+
from rest_framework.response import Response
|
|
17
|
+
|
|
18
|
+
from ..services import StatisticsService, SystemHealthService
|
|
19
|
+
from ..serializers import ActivityEntrySerializer, QuickActionSerializer
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ActivityViewSet(viewsets.GenericViewSet):
|
|
25
|
+
"""
|
|
26
|
+
Activity Tracking ViewSet
|
|
27
|
+
|
|
28
|
+
Provides endpoints for recent activity and quick actions.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
authentication_classes = [SessionAuthentication, BasicAuthentication]
|
|
32
|
+
permission_classes = [IsAdminUser]
|
|
33
|
+
serializer_class = ActivityEntrySerializer
|
|
34
|
+
|
|
35
|
+
@extend_schema(
|
|
36
|
+
summary="Get recent activity",
|
|
37
|
+
description="Retrieve recent system activity entries",
|
|
38
|
+
parameters=[
|
|
39
|
+
OpenApiParameter(
|
|
40
|
+
name='limit',
|
|
41
|
+
description='Maximum number of entries to return',
|
|
42
|
+
required=False,
|
|
43
|
+
type=int,
|
|
44
|
+
default=10
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
responses=ActivityEntrySerializer(many=True),
|
|
48
|
+
tags=["Dashboard - Activity"]
|
|
49
|
+
)
|
|
50
|
+
@action(detail=False, methods=['get'], url_path='recent', pagination_class=None, serializer_class=ActivityEntrySerializer)
|
|
51
|
+
def recent(self, request):
|
|
52
|
+
"""Get recent activity entries."""
|
|
53
|
+
try:
|
|
54
|
+
limit = int(request.query_params.get('limit', 10))
|
|
55
|
+
stats_service = StatisticsService()
|
|
56
|
+
activity = stats_service.get_recent_activity(limit=limit)
|
|
57
|
+
return Response(activity)
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Recent activity API error: {e}")
|
|
61
|
+
return Response({
|
|
62
|
+
'error': str(e)
|
|
63
|
+
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
64
|
+
|
|
65
|
+
@extend_schema(
|
|
66
|
+
summary="Get quick actions",
|
|
67
|
+
description="Retrieve quick action buttons for dashboard",
|
|
68
|
+
responses=QuickActionSerializer(many=True),
|
|
69
|
+
tags=["Dashboard - Activity"]
|
|
70
|
+
)
|
|
71
|
+
@action(detail=False, methods=['get'], url_path='actions', pagination_class=None, serializer_class=QuickActionSerializer)
|
|
72
|
+
def actions(self, request):
|
|
73
|
+
"""Get quick action buttons."""
|
|
74
|
+
try:
|
|
75
|
+
health_service = SystemHealthService()
|
|
76
|
+
actions = health_service.get_quick_actions()
|
|
77
|
+
return Response(actions)
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Quick actions API error: {e}")
|
|
81
|
+
return Response({
|
|
82
|
+
'error': str(e)
|
|
83
|
+
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|