django-cfg 1.4.88__py3-none-any.whl → 1.4.89__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/centrifugo/views/__init__.py +0 -2
- django_cfg/apps/dashboard/services/__init__.py +2 -0
- django_cfg/apps/dashboard/services/overview_service.py +205 -0
- django_cfg/apps/frontend/test_routing.py +134 -0
- django_cfg/apps/frontend/views.py +69 -28
- django_cfg/apps/urls.py +0 -1
- django_cfg/core/builders/apps_builder.py +0 -58
- django_cfg/modules/django_unfold/__init__.py +5 -24
- django_cfg/modules/django_unfold/models/__init__.py +0 -23
- django_cfg/modules/django_unfold/models/config.py +11 -65
- django_cfg/modules/django_unfold/{dashboard.py → navigation.py} +21 -152
- django_cfg/modules/django_unfold/tailwind.py +2 -4
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/third_party.py +0 -9
- django_cfg/routing/callbacks.py +1 -43
- django_cfg/static/frontend/admin/404/index.html +1 -1
- django_cfg/static/frontend/admin/404.html +1 -1
- django_cfg/static/frontend/admin/500/index.html +1 -1
- django_cfg/static/frontend/admin/_next/static/{D_d9HRw5Yn7BRHAX5q89_ → 0sN9ktsgXH48ygtGSrhfu}/_buildManifest.js +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/50314-9443faa6df24aebf.js +1 -0
- django_cfg/static/frontend/admin/_next/static/chunks/pages/{_app-1c0fff0f59a6d683.js → _app-c7dcd3aa616fab68.js} +1 -1
- django_cfg/static/frontend/admin/_next/static/chunks/pages/private/{centrifugo-44a8313fa040e9ad.js → centrifugo-f9ecbc3ae0052a03.js} +1 -1
- django_cfg/static/frontend/admin/auth/index.html +1 -1
- django_cfg/static/frontend/admin/index.html +1 -1
- django_cfg/static/frontend/admin/legal/cookies/index.html +1 -1
- django_cfg/static/frontend/admin/legal/privacy/index.html +1 -1
- django_cfg/static/frontend/admin/legal/security/index.html +1 -1
- django_cfg/static/frontend/admin/legal/terms/index.html +1 -1
- django_cfg/static/frontend/admin/private/centrifugo/index.html +1 -1
- django_cfg/static/frontend/admin/private/index.html +1 -1
- django_cfg/static/frontend/admin/private/profile/index.html +1 -1
- django_cfg/static/frontend/admin/private/ui/index.html +2 -2
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/METADATA +1 -1
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/RECORD +39 -143
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/css/dashboard.css +0 -260
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_channels.mjs +0 -313
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/live_testing.mjs +0 -803
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/main.mjs +0 -341
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/overview.mjs +0 -432
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/testing.mjs +0 -33
- django_cfg/apps/centrifugo/static/django_cfg_centrifugo/js/dashboard/websocket.mjs +0 -210
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/channels_content.html +0 -46
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/live_channels_content.html +0 -123
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/overview_content.html +0 -45
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/publishes_content.html +0 -84
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/stat_cards.html +0 -53
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/system_status.html +0 -91
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/tab_navigation.html +0 -29
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +0 -415
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/layout/base.html +0 -61
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/pages/dashboard.html +0 -58
- django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/tags/connection_script.html +0 -48
- django_cfg/apps/centrifugo/templatetags/__init__.py +0 -1
- django_cfg/apps/centrifugo/templatetags/centrifugo_tags.py +0 -81
- django_cfg/apps/centrifugo/urls_admin.py +0 -20
- django_cfg/apps/centrifugo/views/dashboard.py +0 -28
- django_cfg/modules/django_dashboard/__init__.py +0 -23
- django_cfg/modules/django_dashboard/components.py +0 -312
- django_cfg/modules/django_dashboard/debug.py +0 -174
- django_cfg/modules/django_dashboard/management/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/debug_dashboard.py +0 -109
- django_cfg/modules/django_dashboard/sections/__init__.py +0 -1
- django_cfg/modules/django_dashboard/sections/base.py +0 -129
- django_cfg/modules/django_dashboard/sections/commands.py +0 -33
- django_cfg/modules/django_dashboard/sections/documentation.py +0 -393
- django_cfg/modules/django_dashboard/sections/overview.py +0 -398
- django_cfg/modules/django_dashboard/sections/stats.py +0 -48
- django_cfg/modules/django_dashboard/sections/system.py +0 -74
- django_cfg/modules/django_dashboard/sections/widgets.py +0 -222
- django_cfg/modules/django_unfold/callbacks/__init__.py +0 -9
- django_cfg/modules/django_unfold/callbacks/actions.py +0 -51
- django_cfg/modules/django_unfold/callbacks/apizones.py +0 -122
- django_cfg/modules/django_unfold/callbacks/base.py +0 -290
- django_cfg/modules/django_unfold/callbacks/charts.py +0 -223
- django_cfg/modules/django_unfold/callbacks/commands.py +0 -40
- django_cfg/modules/django_unfold/callbacks/main.py +0 -322
- django_cfg/modules/django_unfold/callbacks/statistics.py +0 -240
- django_cfg/modules/django_unfold/callbacks/system.py +0 -180
- django_cfg/modules/django_unfold/callbacks/users.py +0 -65
- django_cfg/modules/django_unfold/models/dashboard.py +0 -207
- django_cfg/modules/django_unfold/models/tabs.py +0 -26
- django_cfg/modules/django_unfold/models.py +0 -98
- django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +0 -102
- django_cfg/static/frontend/admin/_next/static/chunks/50314-5ec79b293c2283dd.js +0 -1
- django_cfg/templates/admin/sections/commands_section.html +0 -5
- django_cfg/templates/admin/sections/documentation_section.html +0 -5
- django_cfg/templates/admin/sections/overview_section.html +0 -5
- django_cfg/templates/admin/sections/stats_section.html +0 -5
- django_cfg/templates/admin/sections/system_section.html +0 -5
- django_cfg/templates/admin/sections/widgets_section.html +0 -11
- django_cfg/templates/admin_old/components/action_grid.html +0 -49
- django_cfg/templates/admin_old/components/card.html +0 -50
- django_cfg/templates/admin_old/components/data_table.html +0 -67
- django_cfg/templates/admin_old/components/metric_card.html +0 -39
- django_cfg/templates/admin_old/components/modal.html +0 -58
- django_cfg/templates/admin_old/components/progress_bar.html +0 -20
- django_cfg/templates/admin_old/components/section_header.html +0 -26
- django_cfg/templates/admin_old/components/stat_item.html +0 -32
- django_cfg/templates/admin_old/components/stats_grid.html +0 -72
- django_cfg/templates/admin_old/components/status_badge.html +0 -28
- django_cfg/templates/admin_old/components/user_avatar.html +0 -27
- django_cfg/templates/admin_old/constance/change_list.html +0 -74
- django_cfg/templates/admin_old/constance/includes/default_value.html +0 -24
- django_cfg/templates/admin_old/constance/includes/fieldset_header.html +0 -15
- django_cfg/templates/admin_old/constance/includes/results_list.html +0 -16
- django_cfg/templates/admin_old/constance/includes/setting_row.html +0 -50
- django_cfg/templates/admin_old/constance/includes/table_headers.html +0 -10
- django_cfg/templates/admin_old/examples/component_class_example.html +0 -156
- django_cfg/templates/admin_old/import_export/change_list_export.html +0 -24
- django_cfg/templates/admin_old/import_export/change_list_import.html +0 -24
- django_cfg/templates/admin_old/import_export/change_list_import_export.html +0 -34
- django_cfg/templates/admin_old/index.html +0 -80
- django_cfg/templates/admin_old/index_new.html +0 -119
- django_cfg/templates/admin_old/layouts/base_dashboard.html +0 -62
- django_cfg/templates/admin_old/layouts/dashboard_with_tabs.html +0 -176
- django_cfg/templates/admin_old/sections/commands_section.html +0 -549
- django_cfg/templates/admin_old/sections/documentation_section.html +0 -152
- django_cfg/templates/admin_old/sections/overview_section.html +0 -112
- django_cfg/templates/admin_old/sections/stats_section.html +0 -35
- django_cfg/templates/admin_old/sections/system_section.html +0 -99
- django_cfg/templates/admin_old/sections/widgets_section.html +0 -129
- django_cfg/templates/admin_old/snippets/components/activity_tracker.html +0 -70
- django_cfg/templates/admin_old/snippets/components/charts_section.html +0 -113
- django_cfg/templates/admin_old/snippets/components/django_commands.html +0 -270
- django_cfg/templates/admin_old/snippets/components/quick_actions.html +0 -66
- django_cfg/templates/admin_old/snippets/components/recent_activity_improved.html +0 -25
- django_cfg/templates/admin_old/snippets/components/recent_users_table.html +0 -102
- django_cfg/templates/admin_old/snippets/components/stats_cards.html +0 -4
- django_cfg/templates/admin_old/snippets/components/stats_tiles.html +0 -92
- django_cfg/templates/admin_old/snippets/components/system_health.html +0 -22
- django_cfg/templates/admin_old/snippets/components/system_metrics.html +0 -199
- django_cfg/templates/admin_old/snippets/components/user_permissions.html +0 -57
- django_cfg/templates/admin_old/snippets/tabs/app_stats_tab.html +0 -201
- django_cfg/templates/admin_old/snippets/tabs/commands_tab.html +0 -114
- django_cfg/templates/admin_old/snippets/tabs/documentation_tab.html +0 -42
- django_cfg/templates/admin_old/snippets/tabs/overview_tab.html +0 -116
- django_cfg/templates/admin_old/snippets/tabs/stats_tab.html +0 -89
- django_cfg/templates/admin_old/snippets/tabs/users_tab.html +0 -51
- django_cfg/templates/admin_old/snippets/tabs/widgets_tab.html +0 -38
- django_cfg/templates/admin_old/snippets/zones/zones_table.html +0 -176
- /django_cfg/static/frontend/admin/_next/static/{D_d9HRw5Yn7BRHAX5q89_ → 0sN9ktsgXH48ygtGSrhfu}/_ssgManifest.js +0 -0
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.88.dist-info → django_cfg-1.4.89.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Statistics callbacks for dashboard.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from datetime import timedelta
|
|
7
|
-
from typing import Any, Dict, List
|
|
8
|
-
|
|
9
|
-
from django.apps import apps
|
|
10
|
-
from django.contrib.auth import get_user_model
|
|
11
|
-
from django.utils import timezone
|
|
12
|
-
|
|
13
|
-
from django_cfg.modules.django_admin.icons import Icons
|
|
14
|
-
|
|
15
|
-
from ..models.dashboard import StatCard
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class StatisticsCallbacks:
|
|
21
|
-
"""Statistics-related callbacks."""
|
|
22
|
-
|
|
23
|
-
def _get_user_model(self):
|
|
24
|
-
"""Get the user model safely."""
|
|
25
|
-
return get_user_model()
|
|
26
|
-
|
|
27
|
-
def get_user_statistics(self) -> List[StatCard]:
|
|
28
|
-
"""Get user-related statistics as Pydantic models."""
|
|
29
|
-
try:
|
|
30
|
-
User = self._get_user_model()
|
|
31
|
-
|
|
32
|
-
total_users = User.objects.count()
|
|
33
|
-
active_users = User.objects.filter(is_active=True).count()
|
|
34
|
-
new_users_7d = User.objects.filter(
|
|
35
|
-
date_joined__gte=timezone.now() - timedelta(days=7)
|
|
36
|
-
).count()
|
|
37
|
-
staff_users = User.objects.filter(is_staff=True).count()
|
|
38
|
-
|
|
39
|
-
return [
|
|
40
|
-
StatCard(
|
|
41
|
-
title="Total Users",
|
|
42
|
-
value=f"{total_users:,}",
|
|
43
|
-
icon=Icons.PEOPLE,
|
|
44
|
-
change=f"+{new_users_7d}" if new_users_7d > 0 else None,
|
|
45
|
-
change_type="positive" if new_users_7d > 0 else "neutral",
|
|
46
|
-
description="Registered users",
|
|
47
|
-
),
|
|
48
|
-
StatCard(
|
|
49
|
-
title="Active Users",
|
|
50
|
-
value=f"{active_users:,}",
|
|
51
|
-
icon=Icons.PERSON,
|
|
52
|
-
change=(
|
|
53
|
-
f"{(active_users/total_users*100):.1f}%"
|
|
54
|
-
if total_users > 0
|
|
55
|
-
else "0%"
|
|
56
|
-
),
|
|
57
|
-
change_type=(
|
|
58
|
-
"positive" if active_users > total_users * 0.7 else "neutral"
|
|
59
|
-
),
|
|
60
|
-
description="Currently active",
|
|
61
|
-
),
|
|
62
|
-
StatCard(
|
|
63
|
-
title="New This Week",
|
|
64
|
-
value=f"{new_users_7d:,}",
|
|
65
|
-
icon=Icons.PERSON_ADD,
|
|
66
|
-
change_type="positive" if new_users_7d > 0 else "neutral",
|
|
67
|
-
description="Last 7 days",
|
|
68
|
-
),
|
|
69
|
-
StatCard(
|
|
70
|
-
title="Staff Members",
|
|
71
|
-
value=f"{staff_users:,}",
|
|
72
|
-
icon=Icons.ADMIN_PANEL_SETTINGS,
|
|
73
|
-
change=(
|
|
74
|
-
f"{(staff_users/total_users*100):.1f}%" if total_users > 0 else "0%"
|
|
75
|
-
),
|
|
76
|
-
change_type="neutral",
|
|
77
|
-
description="Administrative access",
|
|
78
|
-
),
|
|
79
|
-
]
|
|
80
|
-
except Exception as e:
|
|
81
|
-
logger.error(f"Error getting user statistics: {e}")
|
|
82
|
-
return [
|
|
83
|
-
StatCard(
|
|
84
|
-
title="Users",
|
|
85
|
-
value="N/A",
|
|
86
|
-
icon=Icons.PEOPLE,
|
|
87
|
-
description="Data unavailable",
|
|
88
|
-
)
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
def get_support_statistics(self) -> List[StatCard]:
|
|
92
|
-
"""Get support ticket statistics as Pydantic models."""
|
|
93
|
-
try:
|
|
94
|
-
# Check if support is enabled
|
|
95
|
-
if not self.is_support_enabled():
|
|
96
|
-
return []
|
|
97
|
-
|
|
98
|
-
from django_cfg.apps.support.models import Ticket
|
|
99
|
-
|
|
100
|
-
total_tickets = Ticket.objects.count()
|
|
101
|
-
open_tickets = Ticket.objects.filter(status='open').count()
|
|
102
|
-
resolved_tickets = Ticket.objects.filter(status='resolved').count()
|
|
103
|
-
new_tickets_7d = Ticket.objects.filter(
|
|
104
|
-
created_at__gte=timezone.now() - timedelta(days=7)
|
|
105
|
-
).count()
|
|
106
|
-
|
|
107
|
-
return [
|
|
108
|
-
StatCard(
|
|
109
|
-
title="Total Tickets",
|
|
110
|
-
value=f"{total_tickets:,}",
|
|
111
|
-
icon=Icons.SUPPORT_AGENT,
|
|
112
|
-
change=f"+{new_tickets_7d}" if new_tickets_7d > 0 else None,
|
|
113
|
-
change_type="positive" if new_tickets_7d > 0 else "neutral",
|
|
114
|
-
description="All support tickets",
|
|
115
|
-
),
|
|
116
|
-
StatCard(
|
|
117
|
-
title="Open Tickets",
|
|
118
|
-
value=f"{open_tickets:,}",
|
|
119
|
-
icon=Icons.PENDING,
|
|
120
|
-
change=(
|
|
121
|
-
f"{(open_tickets/total_tickets*100):.1f}%"
|
|
122
|
-
if total_tickets > 0
|
|
123
|
-
else "0%"
|
|
124
|
-
),
|
|
125
|
-
change_type=(
|
|
126
|
-
"negative" if open_tickets > total_tickets * 0.3
|
|
127
|
-
else "positive" if open_tickets == 0
|
|
128
|
-
else "neutral"
|
|
129
|
-
),
|
|
130
|
-
description="Awaiting response",
|
|
131
|
-
),
|
|
132
|
-
StatCard(
|
|
133
|
-
title="Resolved",
|
|
134
|
-
value=f"{resolved_tickets:,}",
|
|
135
|
-
icon=Icons.CHECK_CIRCLE,
|
|
136
|
-
change=(
|
|
137
|
-
f"{(resolved_tickets/total_tickets*100):.1f}%"
|
|
138
|
-
if total_tickets > 0
|
|
139
|
-
else "0%"
|
|
140
|
-
),
|
|
141
|
-
change_type="positive",
|
|
142
|
-
description="Successfully resolved",
|
|
143
|
-
),
|
|
144
|
-
StatCard(
|
|
145
|
-
title="New This Week",
|
|
146
|
-
value=f"{new_tickets_7d:,}",
|
|
147
|
-
icon=Icons.NEW_RELEASES,
|
|
148
|
-
change_type="positive" if new_tickets_7d > 0 else "neutral",
|
|
149
|
-
description="Last 7 days",
|
|
150
|
-
),
|
|
151
|
-
]
|
|
152
|
-
except Exception as e:
|
|
153
|
-
logger.error(f"Error getting support statistics: {e}")
|
|
154
|
-
return [
|
|
155
|
-
StatCard(
|
|
156
|
-
title="Support",
|
|
157
|
-
value="N/A",
|
|
158
|
-
icon=Icons.SUPPORT_AGENT,
|
|
159
|
-
description="Data unavailable",
|
|
160
|
-
)
|
|
161
|
-
]
|
|
162
|
-
|
|
163
|
-
def get_app_statistics(self) -> Dict[str, Any]:
|
|
164
|
-
"""Get statistics for all apps and their models."""
|
|
165
|
-
stats = {"apps": {}, "total_records": 0, "total_models": 0, "total_apps": 0}
|
|
166
|
-
|
|
167
|
-
# Get all installed apps
|
|
168
|
-
for app_config in apps.get_app_configs():
|
|
169
|
-
app_label = app_config.label
|
|
170
|
-
|
|
171
|
-
# Skip system apps
|
|
172
|
-
if app_label in ["admin", "contenttypes", "sessions", "auth"]:
|
|
173
|
-
continue
|
|
174
|
-
|
|
175
|
-
app_stats = self._get_app_stats(app_label)
|
|
176
|
-
if app_stats:
|
|
177
|
-
stats["apps"][app_label] = app_stats
|
|
178
|
-
stats["total_records"] += app_stats.get("total_records", 0)
|
|
179
|
-
stats["total_models"] += app_stats.get("model_count", 0)
|
|
180
|
-
stats["total_apps"] += 1
|
|
181
|
-
|
|
182
|
-
return stats
|
|
183
|
-
|
|
184
|
-
def _get_app_stats(self, app_label: str) -> Dict[str, Any]:
|
|
185
|
-
"""Get statistics for a specific app."""
|
|
186
|
-
try:
|
|
187
|
-
app_config = apps.get_app_config(app_label)
|
|
188
|
-
# Convert generator to list to avoid len() error
|
|
189
|
-
models_list = list(app_config.get_models())
|
|
190
|
-
|
|
191
|
-
if not models_list:
|
|
192
|
-
return None
|
|
193
|
-
|
|
194
|
-
app_stats = {
|
|
195
|
-
"name": app_config.verbose_name or app_label.title(),
|
|
196
|
-
"models": {},
|
|
197
|
-
"total_records": 0,
|
|
198
|
-
"model_count": len(models_list),
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
for model in models_list:
|
|
202
|
-
try:
|
|
203
|
-
# Get model statistics
|
|
204
|
-
model_stats = self._get_model_stats(model)
|
|
205
|
-
if model_stats:
|
|
206
|
-
app_stats["models"][model._meta.model_name] = model_stats
|
|
207
|
-
app_stats["total_records"] += model_stats.get("count", 0)
|
|
208
|
-
except Exception:
|
|
209
|
-
continue
|
|
210
|
-
|
|
211
|
-
return app_stats
|
|
212
|
-
|
|
213
|
-
except Exception:
|
|
214
|
-
return None
|
|
215
|
-
|
|
216
|
-
def _get_model_stats(self, model) -> Dict[str, Any]:
|
|
217
|
-
"""Get statistics for a specific model."""
|
|
218
|
-
try:
|
|
219
|
-
# Get basic model info
|
|
220
|
-
model_stats = {
|
|
221
|
-
"name": model._meta.verbose_name_plural
|
|
222
|
-
or model._meta.verbose_name
|
|
223
|
-
or model._meta.model_name,
|
|
224
|
-
"count": model.objects.count(),
|
|
225
|
-
"fields_count": len(model._meta.fields),
|
|
226
|
-
"admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return model_stats
|
|
230
|
-
|
|
231
|
-
except Exception:
|
|
232
|
-
return None
|
|
233
|
-
|
|
234
|
-
def is_support_enabled(self) -> bool:
|
|
235
|
-
"""Check if support module is enabled."""
|
|
236
|
-
try:
|
|
237
|
-
from django_cfg.apps.support.models import Ticket
|
|
238
|
-
return True
|
|
239
|
-
except ImportError:
|
|
240
|
-
return False
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
System health and metrics callbacks.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
import shutil
|
|
7
|
-
from typing import Any, Dict, List
|
|
8
|
-
|
|
9
|
-
from django.core.cache import cache
|
|
10
|
-
from django.db import connection
|
|
11
|
-
from django.utils import timezone
|
|
12
|
-
|
|
13
|
-
from ..models.dashboard import SystemHealthItem
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class SystemCallbacks:
|
|
19
|
-
"""System health and metrics callbacks."""
|
|
20
|
-
|
|
21
|
-
def get_system_health(self) -> List[SystemHealthItem]:
|
|
22
|
-
"""Get system health status as Pydantic models."""
|
|
23
|
-
health_items = []
|
|
24
|
-
|
|
25
|
-
# Database health
|
|
26
|
-
try:
|
|
27
|
-
with connection.cursor() as cursor:
|
|
28
|
-
cursor.execute("SELECT 1")
|
|
29
|
-
health_items.append(
|
|
30
|
-
SystemHealthItem(
|
|
31
|
-
component="database",
|
|
32
|
-
status="healthy",
|
|
33
|
-
description="Connection successful",
|
|
34
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
|
35
|
-
health_percentage=95,
|
|
36
|
-
)
|
|
37
|
-
)
|
|
38
|
-
except Exception as e:
|
|
39
|
-
health_items.append(
|
|
40
|
-
SystemHealthItem(
|
|
41
|
-
component="database",
|
|
42
|
-
status="error",
|
|
43
|
-
description=f"Connection failed: {str(e)[:50]}",
|
|
44
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
|
45
|
-
health_percentage=0,
|
|
46
|
-
)
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
# Cache health
|
|
50
|
-
try:
|
|
51
|
-
cache.set("health_check", "ok", 10)
|
|
52
|
-
if cache.get("health_check") == "ok":
|
|
53
|
-
health_items.append(
|
|
54
|
-
SystemHealthItem(
|
|
55
|
-
component="cache",
|
|
56
|
-
status="healthy",
|
|
57
|
-
description="Cache operational",
|
|
58
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
|
59
|
-
health_percentage=90,
|
|
60
|
-
)
|
|
61
|
-
)
|
|
62
|
-
else:
|
|
63
|
-
health_items.append(
|
|
64
|
-
SystemHealthItem(
|
|
65
|
-
component="cache",
|
|
66
|
-
status="warning",
|
|
67
|
-
description="Cache not responding",
|
|
68
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
|
69
|
-
health_percentage=50,
|
|
70
|
-
)
|
|
71
|
-
)
|
|
72
|
-
except Exception:
|
|
73
|
-
health_items.append(
|
|
74
|
-
SystemHealthItem(
|
|
75
|
-
component="cache",
|
|
76
|
-
status="unknown",
|
|
77
|
-
description="Cache not configured",
|
|
78
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
|
79
|
-
health_percentage=0,
|
|
80
|
-
)
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
# Storage health
|
|
84
|
-
try:
|
|
85
|
-
total, used, free = shutil.disk_usage("/")
|
|
86
|
-
usage_percentage = (used / total) * 100
|
|
87
|
-
free_percentage = 100 - usage_percentage
|
|
88
|
-
|
|
89
|
-
if free_percentage > 20:
|
|
90
|
-
status = "healthy"
|
|
91
|
-
desc = f"Disk space: {free_percentage:.1f}% free"
|
|
92
|
-
elif free_percentage > 10:
|
|
93
|
-
status = "warning"
|
|
94
|
-
desc = f"Low disk space: {free_percentage:.1f}% free"
|
|
95
|
-
else:
|
|
96
|
-
status = "error"
|
|
97
|
-
desc = f"Critical disk space: {free_percentage:.1f}% free"
|
|
98
|
-
|
|
99
|
-
health_items.append(
|
|
100
|
-
SystemHealthItem(
|
|
101
|
-
component="storage",
|
|
102
|
-
status=status,
|
|
103
|
-
description=desc,
|
|
104
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
|
105
|
-
health_percentage=int(free_percentage),
|
|
106
|
-
)
|
|
107
|
-
)
|
|
108
|
-
except Exception:
|
|
109
|
-
health_items.append(
|
|
110
|
-
SystemHealthItem(
|
|
111
|
-
component="storage",
|
|
112
|
-
status="error",
|
|
113
|
-
description="Storage check failed",
|
|
114
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
|
115
|
-
health_percentage=0,
|
|
116
|
-
)
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# API health
|
|
120
|
-
health_items.append(
|
|
121
|
-
SystemHealthItem(
|
|
122
|
-
component="api",
|
|
123
|
-
status="healthy",
|
|
124
|
-
description="API server running",
|
|
125
|
-
last_check=timezone.now().strftime("%H:%M:%S"),
|
|
126
|
-
health_percentage=100,
|
|
127
|
-
)
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
return health_items
|
|
131
|
-
|
|
132
|
-
def get_system_metrics(self) -> Dict[str, Any]:
|
|
133
|
-
"""Get system metrics for dashboard."""
|
|
134
|
-
metrics = {}
|
|
135
|
-
|
|
136
|
-
# Database metrics
|
|
137
|
-
try:
|
|
138
|
-
with connection.cursor() as cursor:
|
|
139
|
-
cursor.execute("SELECT 1")
|
|
140
|
-
metrics["database"] = {
|
|
141
|
-
"status": "healthy",
|
|
142
|
-
"type": "PostgreSQL",
|
|
143
|
-
"health_percentage": 95,
|
|
144
|
-
"description": "Connection successful",
|
|
145
|
-
}
|
|
146
|
-
except Exception as e:
|
|
147
|
-
metrics["database"] = {
|
|
148
|
-
"status": "error",
|
|
149
|
-
"type": "PostgreSQL",
|
|
150
|
-
"health_percentage": 0,
|
|
151
|
-
"description": f"Connection failed: {str(e)}",
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
# Cache metrics
|
|
155
|
-
try:
|
|
156
|
-
cache.set("health_check", "ok", 10)
|
|
157
|
-
cache_result = cache.get("health_check")
|
|
158
|
-
if cache_result == "ok":
|
|
159
|
-
metrics["cache"] = {
|
|
160
|
-
"status": "healthy",
|
|
161
|
-
"type": "Memory Cache",
|
|
162
|
-
"health_percentage": 90,
|
|
163
|
-
"description": "Cache working properly",
|
|
164
|
-
}
|
|
165
|
-
else:
|
|
166
|
-
metrics["cache"] = {
|
|
167
|
-
"status": "warning",
|
|
168
|
-
"type": "Memory Cache",
|
|
169
|
-
"health_percentage": 50,
|
|
170
|
-
"description": "Cache response delayed",
|
|
171
|
-
}
|
|
172
|
-
except Exception as e:
|
|
173
|
-
metrics["cache"] = {
|
|
174
|
-
"status": "error",
|
|
175
|
-
"type": "Memory Cache",
|
|
176
|
-
"health_percentage": 0,
|
|
177
|
-
"description": f"Cache error: {str(e)}",
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return metrics
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Users data callbacks.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from typing import Any, Dict, List
|
|
7
|
-
|
|
8
|
-
from django.contrib.auth import get_user_model
|
|
9
|
-
|
|
10
|
-
from .base import get_user_admin_urls
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class UsersCallbacks:
|
|
16
|
-
"""Users data callbacks."""
|
|
17
|
-
|
|
18
|
-
def _get_user_model(self):
|
|
19
|
-
"""Get the user model safely."""
|
|
20
|
-
return get_user_model()
|
|
21
|
-
|
|
22
|
-
def get_recent_users(self) -> List[Dict[str, Any]]:
|
|
23
|
-
"""Get recent users data for template."""
|
|
24
|
-
try:
|
|
25
|
-
# Avoid database access during app initialization
|
|
26
|
-
from django.apps import apps
|
|
27
|
-
if not apps.ready:
|
|
28
|
-
return []
|
|
29
|
-
|
|
30
|
-
User = self._get_user_model()
|
|
31
|
-
recent_users = User.objects.select_related().order_by("-date_joined")[:10]
|
|
32
|
-
|
|
33
|
-
# Get admin URLs for user model
|
|
34
|
-
user_admin_urls = get_user_admin_urls()
|
|
35
|
-
|
|
36
|
-
return [
|
|
37
|
-
{
|
|
38
|
-
"id": user.id,
|
|
39
|
-
"username": user.username,
|
|
40
|
-
"email": user.email or "No email",
|
|
41
|
-
"date_joined": (
|
|
42
|
-
user.date_joined.strftime("%Y-%m-%d")
|
|
43
|
-
if user.date_joined
|
|
44
|
-
else "Unknown"
|
|
45
|
-
),
|
|
46
|
-
"is_active": user.is_active,
|
|
47
|
-
"is_staff": user.is_staff,
|
|
48
|
-
"is_superuser": user.is_superuser,
|
|
49
|
-
"last_login": user.last_login,
|
|
50
|
-
"admin_urls": {
|
|
51
|
-
"change": (
|
|
52
|
-
user_admin_urls["change"].format(id=user.id)
|
|
53
|
-
if user.id
|
|
54
|
-
else None
|
|
55
|
-
),
|
|
56
|
-
"view": (
|
|
57
|
-
user_admin_urls["view"].format(id=user.id) if user.id else None
|
|
58
|
-
),
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
for user in recent_users
|
|
62
|
-
]
|
|
63
|
-
except Exception as e:
|
|
64
|
-
logger.error(f"Error getting recent users: {e}")
|
|
65
|
-
return []
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Dashboard Components Models for Unfold
|
|
3
|
-
|
|
4
|
-
Pydantic models for dashboard components like stat cards, health items, etc.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from typing import Any, Dict, List, Literal, Optional
|
|
8
|
-
|
|
9
|
-
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class StatCard(BaseModel):
|
|
13
|
-
"""Dashboard statistics card model."""
|
|
14
|
-
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
15
|
-
|
|
16
|
-
title: str = Field(..., description="Card title")
|
|
17
|
-
value: str = Field(..., description="Main value to display")
|
|
18
|
-
icon: str = Field(..., description="Material icon name")
|
|
19
|
-
change: Optional[str] = Field(None, description="Change indicator (e.g., '+12%')")
|
|
20
|
-
change_type: Literal["positive", "negative", "neutral"] = Field(default="neutral", description="Change type")
|
|
21
|
-
description: Optional[str] = Field(None, description="Additional description")
|
|
22
|
-
color: str = Field("primary", description="Card color theme")
|
|
23
|
-
|
|
24
|
-
@computed_field
|
|
25
|
-
@property
|
|
26
|
-
def css_classes(self) -> Dict[str, str]:
|
|
27
|
-
"""Get CSS classes for different states."""
|
|
28
|
-
return {
|
|
29
|
-
"positive": "text-emerald-600 bg-emerald-100 dark:bg-emerald-900/20 dark:text-emerald-400",
|
|
30
|
-
"negative": "text-red-600 bg-red-100 dark:bg-red-900/20 dark:text-red-400",
|
|
31
|
-
"neutral": "text-slate-600 bg-slate-100 dark:bg-slate-700 dark:text-slate-400"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
35
|
-
"""Convert to dictionary for Unfold dashboard widgets."""
|
|
36
|
-
return {
|
|
37
|
-
"title": self.title,
|
|
38
|
-
"value_template": self.value,
|
|
39
|
-
"icon": self.icon,
|
|
40
|
-
"color": self.color,
|
|
41
|
-
"change": self.change,
|
|
42
|
-
"change_type": self.change_type,
|
|
43
|
-
"description": self.description,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class SystemHealthItem(BaseModel):
|
|
48
|
-
"""System health status item."""
|
|
49
|
-
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
50
|
-
|
|
51
|
-
component: str = Field(..., description="Component name")
|
|
52
|
-
status: Literal["healthy", "warning", "error", "unknown"] = Field(..., description="Health status")
|
|
53
|
-
description: str = Field(..., description="Status description")
|
|
54
|
-
last_check: str = Field(..., description="Last check time")
|
|
55
|
-
health_percentage: Optional[int] = Field(None, description="Health percentage (0-100)")
|
|
56
|
-
|
|
57
|
-
@computed_field
|
|
58
|
-
@property
|
|
59
|
-
def icon(self) -> str:
|
|
60
|
-
"""Get icon based on component type."""
|
|
61
|
-
icons = {
|
|
62
|
-
"database": "storage",
|
|
63
|
-
"cache": "memory",
|
|
64
|
-
"queue": "queue",
|
|
65
|
-
"storage": "folder",
|
|
66
|
-
"api": "api",
|
|
67
|
-
}
|
|
68
|
-
return icons.get(self.component.lower(), "info")
|
|
69
|
-
|
|
70
|
-
@computed_field
|
|
71
|
-
@property
|
|
72
|
-
def status_icon(self) -> str:
|
|
73
|
-
"""Get status icon."""
|
|
74
|
-
icons = {
|
|
75
|
-
"healthy": "check_circle",
|
|
76
|
-
"warning": "warning",
|
|
77
|
-
"error": "error",
|
|
78
|
-
"unknown": "help"
|
|
79
|
-
}
|
|
80
|
-
return icons.get(self.status, "help")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class QuickAction(BaseModel):
|
|
84
|
-
"""Quick action button model."""
|
|
85
|
-
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
86
|
-
|
|
87
|
-
title: str = Field(..., description="Action title")
|
|
88
|
-
description: str = Field(..., description="Action description")
|
|
89
|
-
icon: str = Field(..., description="Material icon name")
|
|
90
|
-
link: str = Field(..., description="Action URL or URL name")
|
|
91
|
-
color: Literal["primary", "success", "warning", "danger", "secondary"] = Field(default="primary", description="Button color theme")
|
|
92
|
-
category: str = Field("general", description="Action category (admin, user, system)")
|
|
93
|
-
|
|
94
|
-
def get_resolved_url(self) -> str:
|
|
95
|
-
"""
|
|
96
|
-
Resolve URL name to full URL if needed.
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
Full URL string - either the original link if it's already a URL,
|
|
100
|
-
or the resolved URL if it's a URL name.
|
|
101
|
-
"""
|
|
102
|
-
# If link starts with '/' or 'http', it's already a full URL
|
|
103
|
-
if self.link.startswith(("/", "http")):
|
|
104
|
-
return self.link
|
|
105
|
-
|
|
106
|
-
# Try to resolve as URL name
|
|
107
|
-
try:
|
|
108
|
-
from django.urls import reverse
|
|
109
|
-
from django.urls.exceptions import NoReverseMatch
|
|
110
|
-
return reverse(self.link)
|
|
111
|
-
except (NoReverseMatch, ImportError, Exception):
|
|
112
|
-
# If reverse fails, return the original link
|
|
113
|
-
return self.link
|
|
114
|
-
|
|
115
|
-
def model_dump(self, **kwargs) -> dict:
|
|
116
|
-
"""Override model_dump to include resolved URL."""
|
|
117
|
-
data = super().model_dump(**kwargs)
|
|
118
|
-
# Replace link with resolved URL
|
|
119
|
-
data["link"] = self.get_resolved_url()
|
|
120
|
-
return data
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class DashboardWidget(BaseModel):
|
|
124
|
-
"""Dashboard widget configuration."""
|
|
125
|
-
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
126
|
-
|
|
127
|
-
title: str = Field(..., description="Widget title")
|
|
128
|
-
template: Optional[str] = Field(None, description="Custom template path")
|
|
129
|
-
callback: Optional[str] = Field(None, description="Callback function path")
|
|
130
|
-
width: int = Field(12, description="Widget width (1-12)")
|
|
131
|
-
order: int = Field(0, description="Widget order")
|
|
132
|
-
|
|
133
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
134
|
-
"""Convert to dictionary for Unfold dashboard widgets."""
|
|
135
|
-
return {
|
|
136
|
-
"title": self.title,
|
|
137
|
-
"template": self.template,
|
|
138
|
-
"callback": self.callback,
|
|
139
|
-
"width": self.width,
|
|
140
|
-
"order": self.order,
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
class StatsCardsWidget(BaseModel):
|
|
145
|
-
"""Stats cards widget for dashboard."""
|
|
146
|
-
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
147
|
-
|
|
148
|
-
type: Literal["stats_cards"] = Field(default="stats_cards", description="Widget type")
|
|
149
|
-
title: str = Field(..., description="Widget title")
|
|
150
|
-
cards: List[StatCard] = Field(default_factory=list, description="Statistics cards")
|
|
151
|
-
|
|
152
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
153
|
-
"""Convert to dictionary for Unfold dashboard widgets."""
|
|
154
|
-
return {
|
|
155
|
-
"type": self.type,
|
|
156
|
-
"title": self.title,
|
|
157
|
-
"cards": [card.to_dict() for card in self.cards],
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class ChartDataset(BaseModel):
|
|
162
|
-
"""Chart dataset for dashboard charts."""
|
|
163
|
-
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
164
|
-
|
|
165
|
-
label: str = Field(..., description="Dataset label")
|
|
166
|
-
data: List[int] = Field(default_factory=list, description="Data points")
|
|
167
|
-
backgroundColor: str = Field(..., description="Background color")
|
|
168
|
-
borderColor: str = Field(..., description="Border color")
|
|
169
|
-
tension: float = Field(0.4, description="Line tension")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
class ChartData(BaseModel):
|
|
173
|
-
"""Chart data structure."""
|
|
174
|
-
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
175
|
-
|
|
176
|
-
labels: List[str] = Field(default_factory=list, description="Chart labels")
|
|
177
|
-
datasets: List[ChartDataset] = Field(default_factory=list, description="Chart datasets")
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
class DashboardData(BaseModel):
|
|
181
|
-
"""Main dashboard data container."""
|
|
182
|
-
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
183
|
-
|
|
184
|
-
# Statistics cards
|
|
185
|
-
stat_cards: List[StatCard] = Field(default_factory=list, description="Dashboard statistics cards")
|
|
186
|
-
|
|
187
|
-
# System health
|
|
188
|
-
system_health: List[SystemHealthItem] = Field(default_factory=list, description="System health items")
|
|
189
|
-
|
|
190
|
-
# Quick actions
|
|
191
|
-
quick_actions: List[QuickAction] = Field(default_factory=list, description="Quick action buttons")
|
|
192
|
-
|
|
193
|
-
# Additional data
|
|
194
|
-
last_updated: str = Field(..., description="Last update timestamp")
|
|
195
|
-
environment: str = Field("development", description="Current environment")
|
|
196
|
-
|
|
197
|
-
@computed_field
|
|
198
|
-
@property
|
|
199
|
-
def total_users(self) -> int:
|
|
200
|
-
"""Get total users from stat cards."""
|
|
201
|
-
for card in self.stat_cards:
|
|
202
|
-
if "user" in card.title.lower():
|
|
203
|
-
try:
|
|
204
|
-
return int(card.value.replace(",", ""))
|
|
205
|
-
except (ValueError, AttributeError):
|
|
206
|
-
pass
|
|
207
|
-
return 0
|