django-cfg 1.4.120__py3-none-any.whl → 1.5.2__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 +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
- django_cfg/apps/dashboard/serializers/__init__.py +0 -12
- django_cfg/apps/dashboard/serializers/activity.py +1 -1
- django_cfg/apps/dashboard/services/__init__.py +0 -2
- django_cfg/apps/dashboard/services/charts_service.py +4 -3
- django_cfg/apps/dashboard/services/statistics_service.py +11 -2
- django_cfg/apps/dashboard/services/system_health_service.py +64 -106
- django_cfg/apps/dashboard/urls.py +0 -2
- django_cfg/apps/dashboard/views/__init__.py +0 -2
- django_cfg/apps/dashboard/views/commands_views.py +3 -6
- django_cfg/apps/dashboard/views/overview_views.py +14 -13
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/rq/__init__.py +9 -0
- django_cfg/apps/rq/apps.py +80 -0
- django_cfg/apps/rq/management/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
- django_cfg/apps/rq/management/commands/rqstats.py +33 -0
- django_cfg/apps/rq/management/commands/rqworker.py +31 -0
- django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
- django_cfg/apps/rq/serializers/__init__.py +40 -0
- django_cfg/apps/rq/serializers/health.py +60 -0
- django_cfg/apps/rq/serializers/job.py +100 -0
- django_cfg/apps/rq/serializers/queue.py +80 -0
- django_cfg/apps/rq/serializers/schedule.py +178 -0
- django_cfg/apps/rq/serializers/testing.py +139 -0
- django_cfg/apps/rq/serializers/worker.py +58 -0
- django_cfg/apps/rq/services/__init__.py +25 -0
- django_cfg/apps/rq/services/config_helper.py +233 -0
- django_cfg/apps/rq/services/models/README.md +417 -0
- django_cfg/apps/rq/services/models/__init__.py +30 -0
- django_cfg/apps/rq/services/models/event.py +123 -0
- django_cfg/apps/rq/services/models/job.py +99 -0
- django_cfg/apps/rq/services/models/queue.py +92 -0
- django_cfg/apps/rq/services/models/worker.py +104 -0
- django_cfg/apps/rq/services/rq_converters.py +183 -0
- django_cfg/apps/rq/tasks/__init__.py +23 -0
- django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
- django_cfg/apps/rq/urls.py +54 -0
- django_cfg/apps/rq/views/__init__.py +19 -0
- django_cfg/apps/rq/views/jobs.py +882 -0
- django_cfg/apps/rq/views/monitoring.py +248 -0
- django_cfg/apps/rq/views/queues.py +261 -0
- django_cfg/apps/rq/views/schedule.py +400 -0
- django_cfg/apps/rq/views/testing.py +761 -0
- django_cfg/apps/rq/views/workers.py +195 -0
- django_cfg/apps/urls.py +13 -8
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +16 -26
- django_cfg/core/builders/apps_builder.py +7 -11
- django_cfg/core/generation/integration_generators/__init__.py +3 -6
- django_cfg/core/generation/integration_generators/django_rq.py +80 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +15 -15
- django_cfg/core/integration/display/startup.py +6 -20
- django_cfg/mixins/__init__.py +2 -0
- django_cfg/mixins/superadmin_api.py +59 -0
- django_cfg/models/__init__.py +3 -3
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/models/django/__init__.py +3 -3
- django_cfg/models/django/django_rq.py +621 -0
- django_cfg/models/django/revolution_legacy.py +1 -1
- django_cfg/modules/base.py +19 -6
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +205 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_unfold/navigation.py +21 -18
- django_cfg/pyproject.toml +4 -6
- django_cfg/registry/core.py +4 -7
- django_cfg/registry/modules.py +6 -0
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/constance/includes/results_list.html +73 -0
- django_cfg/templates/admin/index.html +187 -62
- django_cfg/templatetags/django_cfg.py +61 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
- django_cfg/apps/dashboard/permissions.py +0 -48
- django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
- django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
- django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
- django_cfg/apps/tasks/__init__.py +0 -64
- django_cfg/apps/tasks/admin/__init__.py +0 -4
- django_cfg/apps/tasks/admin/task_log.py +0 -265
- django_cfg/apps/tasks/apps.py +0 -15
- django_cfg/apps/tasks/filters/__init__.py +0 -10
- django_cfg/apps/tasks/filters/task_log.py +0 -121
- django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
- django_cfg/apps/tasks/models/__init__.py +0 -4
- django_cfg/apps/tasks/models/task_log.py +0 -246
- django_cfg/apps/tasks/serializers/__init__.py +0 -28
- django_cfg/apps/tasks/serializers/task_log.py +0 -249
- django_cfg/apps/tasks/services/__init__.py +0 -10
- django_cfg/apps/tasks/services/client/__init__.py +0 -7
- django_cfg/apps/tasks/services/client/client.py +0 -234
- django_cfg/apps/tasks/services/config_helper.py +0 -63
- django_cfg/apps/tasks/services/sync.py +0 -204
- django_cfg/apps/tasks/urls.py +0 -16
- django_cfg/apps/tasks/views/__init__.py +0 -10
- django_cfg/apps/tasks/views/task_log.py +0 -41
- django_cfg/apps/tasks/views/task_log_base.py +0 -41
- django_cfg/apps/tasks/views/task_log_overview.py +0 -100
- django_cfg/apps/tasks/views/task_log_related.py +0 -41
- django_cfg/apps/tasks/views/task_log_stats.py +0 -91
- django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
- django_cfg/core/generation/integration_generators/django_q2.py +0 -133
- django_cfg/core/generation/integration_generators/tasks.py +0 -88
- django_cfg/models/django/django_q2.py +0 -514
- django_cfg/models/tasks/__init__.py +0 -49
- django_cfg/models/tasks/backends.py +0 -122
- django_cfg/models/tasks/config.py +0 -209
- django_cfg/models/tasks/utils.py +0 -162
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- django_cfg/modules/django_q2/README.md +0 -140
- django_cfg/modules/django_q2/__init__.py +0 -8
- django_cfg/modules/django_q2/apps.py +0 -107
- django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
- /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
- /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -32,11 +32,11 @@ Example:
|
|
|
32
32
|
default_app_config = "django_cfg.apps.DjangoCfgConfig"
|
|
33
33
|
|
|
34
34
|
# Version information
|
|
35
|
-
__version__ = "1.
|
|
35
|
+
__version__ = "1.5.2"
|
|
36
36
|
__license__ = "MIT"
|
|
37
37
|
|
|
38
38
|
# Import registry for organized lazy loading
|
|
39
|
-
from .config import LIB_NAME
|
|
39
|
+
from .config import LIB_NAME, is_feature_available, require_feature, register_feature
|
|
40
40
|
from .registry import DJANGO_CFG_REGISTRY
|
|
41
41
|
|
|
42
42
|
# Get author from library config
|
|
@@ -55,5 +55,9 @@ def __getattr__(name: str):
|
|
|
55
55
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
# Export all registered components
|
|
59
|
-
__all__ = list(DJANGO_CFG_REGISTRY.keys())
|
|
58
|
+
# Export all registered components + feature detection
|
|
59
|
+
__all__ = list(DJANGO_CFG_REGISTRY.keys()) + [
|
|
60
|
+
"is_feature_available",
|
|
61
|
+
"require_feature",
|
|
62
|
+
"register_feature",
|
|
63
|
+
]
|
|
@@ -96,7 +96,7 @@ class CentrifugoLogAdmin(PydanticAdmin):
|
|
|
96
96
|
|
|
97
97
|
try:
|
|
98
98
|
formatted = json.dumps(obj.data, indent=2)
|
|
99
|
-
return
|
|
99
|
+
return self.html.code_block(formatted, language="json", max_height="400px")
|
|
100
100
|
except Exception:
|
|
101
101
|
return str(obj.data)
|
|
102
102
|
|
|
@@ -106,37 +106,22 @@ class CentrifugoLogAdmin(PydanticAdmin):
|
|
|
106
106
|
"""Display error information if publish failed."""
|
|
107
107
|
if obj.is_successful or obj.status == "pending":
|
|
108
108
|
return self.html.inline(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
]
|
|
109
|
+
self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
|
|
110
|
+
self.html.text("No errors", variant="success"),
|
|
111
|
+
separator=" "
|
|
113
112
|
)
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
self.html.inline(
|
|
120
|
-
[
|
|
121
|
-
self.html.span("Error Code:", "font-semibold"),
|
|
122
|
-
self.html.badge(obj.error_code, variant="danger", icon=Icons.ERROR),
|
|
123
|
-
],
|
|
124
|
-
separator=" ",
|
|
125
|
-
)
|
|
126
|
-
)
|
|
114
|
+
error_code_line = self.html.key_value(
|
|
115
|
+
"Error Code",
|
|
116
|
+
self.html.badge(obj.error_code, variant="danger", icon=Icons.ERROR)
|
|
117
|
+
) if obj.error_code else None
|
|
127
118
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
self.html.span("Message:", "font-semibold"),
|
|
133
|
-
self.html.span(obj.error_message, "text-red-600"),
|
|
134
|
-
],
|
|
135
|
-
separator=" ",
|
|
136
|
-
)
|
|
137
|
-
)
|
|
119
|
+
error_msg_line = self.html.key_value(
|
|
120
|
+
"Message",
|
|
121
|
+
self.html.text(obj.error_message, variant="danger")
|
|
122
|
+
) if obj.error_message else None
|
|
138
123
|
|
|
139
|
-
return
|
|
124
|
+
return self.html.breakdown(error_code_line, error_msg_line) if (error_code_line or error_msg_line) else self.html.empty()
|
|
140
125
|
|
|
141
126
|
error_details_display.short_description = "Error Details"
|
|
142
127
|
|
|
@@ -145,57 +130,34 @@ class CentrifugoLogAdmin(PydanticAdmin):
|
|
|
145
130
|
if not obj.wait_for_ack:
|
|
146
131
|
return self.html.empty("No ACK tracking")
|
|
147
132
|
|
|
148
|
-
stats = []
|
|
149
|
-
|
|
150
133
|
# ACK timeout
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
self.html.span("Timeout:", "font-semibold"),
|
|
156
|
-
self.html.span(f"{obj.ack_timeout}s", "text-gray-600"),
|
|
157
|
-
],
|
|
158
|
-
separator=" ",
|
|
159
|
-
)
|
|
160
|
-
)
|
|
134
|
+
timeout_line = self.html.key_value(
|
|
135
|
+
"Timeout",
|
|
136
|
+
f"{obj.ack_timeout}s"
|
|
137
|
+
) if obj.ack_timeout else None
|
|
161
138
|
|
|
162
139
|
# ACKs received
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
self.html.span("ACKs Received:", "font-semibold"),
|
|
167
|
-
self.html.badge(str(obj.acks_received), variant="info"),
|
|
168
|
-
],
|
|
169
|
-
separator=" ",
|
|
170
|
-
)
|
|
140
|
+
received_line = self.html.key_value(
|
|
141
|
+
"ACKs Received",
|
|
142
|
+
self.html.badge(str(obj.acks_received), variant="info")
|
|
171
143
|
)
|
|
172
144
|
|
|
173
145
|
# ACKs expected (if known)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
146
|
+
expected_line = self.html.key_value(
|
|
147
|
+
"ACKs Expected",
|
|
148
|
+
str(obj.acks_expected)
|
|
149
|
+
) if obj.acks_expected else None
|
|
150
|
+
|
|
151
|
+
# Delivery rate
|
|
152
|
+
rate_line = None
|
|
153
|
+
if obj.acks_expected and obj.delivery_rate is not None:
|
|
154
|
+
rate_pct = obj.delivery_rate * 100
|
|
155
|
+
rate_line = self.html.key_value(
|
|
156
|
+
"Delivery Rate",
|
|
157
|
+
self.html.number(rate_pct, precision=1, suffix="%")
|
|
183
158
|
)
|
|
184
159
|
|
|
185
|
-
|
|
186
|
-
if obj.delivery_rate is not None:
|
|
187
|
-
rate_pct = obj.delivery_rate * 100
|
|
188
|
-
stats.append(
|
|
189
|
-
self.html.inline(
|
|
190
|
-
[
|
|
191
|
-
self.html.span("Delivery Rate:", "font-semibold"),
|
|
192
|
-
self.html.span(f"{rate_pct:.1f}%", "text-blue-600"),
|
|
193
|
-
],
|
|
194
|
-
separator=" ",
|
|
195
|
-
)
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
return "<br>".join(stats) if stats else self.html.empty()
|
|
160
|
+
return self.html.breakdown(timeout_line, received_line, expected_line, rate_line)
|
|
199
161
|
|
|
200
162
|
delivery_stats_display.short_description = "Delivery Statistics"
|
|
201
163
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# PostgreSQL Transaction Error Fix
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
Error: **"current transaction is aborted, commands ignored until end of transaction block"**
|
|
6
|
+
|
|
7
|
+
### Root Causes:
|
|
8
|
+
1. Using `.extra()` for raw SQL queries in PostgreSQL
|
|
9
|
+
2. Calling `.count()` on models whose tables don't exist in the database
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
### 1. Replaced `.extra()` with `TruncDate()` in `charts_service.py`
|
|
14
|
+
|
|
15
|
+
**Before:**
|
|
16
|
+
```python
|
|
17
|
+
.extra({'date': "date(date_joined)"}) # ❌ Raw SQL
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**After:**
|
|
21
|
+
```python
|
|
22
|
+
from django.db.models.functions import TruncDate
|
|
23
|
+
.annotate(date=TruncDate('date_joined')) # ✅ Django ORM
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Added table existence check in `statistics_service.py`
|
|
27
|
+
|
|
28
|
+
**Problem:** `model.objects.count()` was called on ALL models, including those without database tables (e.g., migrations not run).
|
|
29
|
+
|
|
30
|
+
**Solution:** Check if table exists before querying:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
def _get_model_stats(self, model) -> Optional[Dict[str, Any]]:
|
|
34
|
+
try:
|
|
35
|
+
from django.db import connection, OperationalError, ProgrammingError
|
|
36
|
+
|
|
37
|
+
# Check if table exists before querying
|
|
38
|
+
table_name = model._meta.db_table
|
|
39
|
+
with connection.cursor() as cursor:
|
|
40
|
+
cursor.execute(f"SELECT COUNT(*) FROM {connection.ops.quote_name(table_name)} LIMIT 1")
|
|
41
|
+
|
|
42
|
+
# Now safe to call model.objects.count()
|
|
43
|
+
model_stats = {
|
|
44
|
+
"count": model.objects.count(),
|
|
45
|
+
...
|
|
46
|
+
}
|
|
47
|
+
return model_stats
|
|
48
|
+
|
|
49
|
+
except (OperationalError, ProgrammingError):
|
|
50
|
+
# Table doesn't exist - skip this model
|
|
51
|
+
return None
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Modified Files
|
|
55
|
+
|
|
56
|
+
- ✅ `services/charts_service.py` - replaced all `.extra()` with `TruncDate()`
|
|
57
|
+
- ✅ `services/statistics_service.py` - added table existence check
|
|
58
|
+
- ✅ `views/overview_views.py` - simplified, removed complex transaction logic
|
|
59
|
+
|
|
60
|
+
## Key Lessons
|
|
61
|
+
|
|
62
|
+
1. **Don't use `.extra()`** - always use Django ORM functions
|
|
63
|
+
2. **Check table existence** before querying models
|
|
64
|
+
3. **Don't overcomplicate** - let Django handle transactions
|
|
65
|
+
4. **Catch specific exceptions** - `OperationalError`, `ProgrammingError` for DB issues
|
|
66
|
+
|
|
67
|
+
## Conclusion
|
|
68
|
+
|
|
69
|
+
Keep it simple! The issue was two simple bugs:
|
|
70
|
+
- Using raw SQL (`.extra()`)
|
|
71
|
+
- Querying non-existent tables
|
|
72
|
+
|
|
73
|
+
Fixed by using proper Django ORM and checking table existence.
|
|
@@ -22,12 +22,6 @@ from .commands import (
|
|
|
22
22
|
CommandHelpResponseSerializer,
|
|
23
23
|
)
|
|
24
24
|
from .apizones import APIZoneSerializer, APIZonesSummarySerializer
|
|
25
|
-
from .django_q2 import (
|
|
26
|
-
DjangoQ2ScheduleSerializer,
|
|
27
|
-
DjangoQ2TaskSerializer,
|
|
28
|
-
DjangoQ2StatusSerializer,
|
|
29
|
-
DjangoQ2SummarySerializer,
|
|
30
|
-
)
|
|
31
25
|
|
|
32
26
|
__all__ = [
|
|
33
27
|
# Base
|
|
@@ -65,10 +59,4 @@ __all__ = [
|
|
|
65
59
|
# API Zones
|
|
66
60
|
'APIZoneSerializer',
|
|
67
61
|
'APIZonesSummarySerializer',
|
|
68
|
-
|
|
69
|
-
# Django-Q2
|
|
70
|
-
'DjangoQ2ScheduleSerializer',
|
|
71
|
-
'DjangoQ2TaskSerializer',
|
|
72
|
-
'DjangoQ2StatusSerializer',
|
|
73
|
-
'DjangoQ2SummarySerializer',
|
|
74
62
|
]
|
|
@@ -19,7 +19,7 @@ class QuickActionSerializer(serializers.Serializer):
|
|
|
19
19
|
icon = serializers.CharField(help_text="Material icon name")
|
|
20
20
|
link = serializers.CharField(help_text="Action URL")
|
|
21
21
|
color = serializers.ChoiceField(
|
|
22
|
-
choices=['primary', 'success', 'warning', 'danger', 'secondary'],
|
|
22
|
+
choices=['primary', 'success', 'warning', 'danger', 'secondary', 'info', 'default'],
|
|
23
23
|
default='primary',
|
|
24
24
|
help_text="Button color theme"
|
|
25
25
|
)
|
|
@@ -11,7 +11,6 @@ from .charts_service import ChartsService
|
|
|
11
11
|
from .commands_service import CommandsService
|
|
12
12
|
from .apizones_service import APIZonesService
|
|
13
13
|
from .overview_service import OverviewService
|
|
14
|
-
from .django_q2_service import DjangoQ2Service
|
|
15
14
|
|
|
16
15
|
__all__ = [
|
|
17
16
|
'StatisticsService',
|
|
@@ -20,5 +19,4 @@ __all__ = [
|
|
|
20
19
|
'CommandsService',
|
|
21
20
|
'APIZonesService',
|
|
22
21
|
'OverviewService',
|
|
23
|
-
'DjangoQ2Service',
|
|
24
22
|
]
|
|
@@ -11,6 +11,7 @@ from typing import Any, Dict, List
|
|
|
11
11
|
|
|
12
12
|
from django.contrib.auth import get_user_model
|
|
13
13
|
from django.db.models import Count
|
|
14
|
+
from django.db.models.functions import TruncDate
|
|
14
15
|
from django.utils import timezone
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
@@ -78,7 +79,7 @@ class ChartsService:
|
|
|
78
79
|
# Get registration counts by date
|
|
79
80
|
registration_data = (
|
|
80
81
|
User.objects.filter(date_joined__date__gte=start_date)
|
|
81
|
-
.
|
|
82
|
+
.annotate(date=TruncDate('date_joined'))
|
|
82
83
|
.values('date')
|
|
83
84
|
.annotate(count=Count('id'))
|
|
84
85
|
.order_by('date')
|
|
@@ -138,7 +139,7 @@ class ChartsService:
|
|
|
138
139
|
# Get login activity (users who logged in each day)
|
|
139
140
|
activity_data = (
|
|
140
141
|
User.objects.filter(last_login__date__gte=start_date, last_login__isnull=False)
|
|
141
|
-
.
|
|
142
|
+
.annotate(date=TruncDate('last_login'))
|
|
142
143
|
.values('date')
|
|
143
144
|
.annotate(count=Count('id'))
|
|
144
145
|
.order_by('date')
|
|
@@ -192,7 +193,7 @@ class ChartsService:
|
|
|
192
193
|
# Get activity data by date
|
|
193
194
|
activity_data = (
|
|
194
195
|
User.objects.filter(last_login__date__gte=start_date, last_login__isnull=False)
|
|
195
|
-
.
|
|
196
|
+
.annotate(date=TruncDate('last_login'))
|
|
196
197
|
.values('date')
|
|
197
198
|
.annotate(count=Count('id'))
|
|
198
199
|
.order_by('date')
|
|
@@ -64,7 +64,7 @@ class StatisticsService:
|
|
|
64
64
|
'superusers': superusers,
|
|
65
65
|
}
|
|
66
66
|
except Exception as e:
|
|
67
|
-
self.logger.error(f"Error getting user statistics: {e}")
|
|
67
|
+
self.logger.error(f"Error getting user statistics: {e}", exc_info=True)
|
|
68
68
|
return {
|
|
69
69
|
'total_users': 0,
|
|
70
70
|
'active_users': 0,
|
|
@@ -145,20 +145,29 @@ class StatisticsService:
|
|
|
145
145
|
|
|
146
146
|
def _get_model_stats(self, model) -> Optional[Dict[str, Any]]:
|
|
147
147
|
"""Get statistics for a specific model."""
|
|
148
|
+
from django.db import OperationalError, ProgrammingError
|
|
149
|
+
|
|
148
150
|
try:
|
|
151
|
+
# Just try to count - if table doesn't exist, exception will be caught
|
|
152
|
+
count = model.objects.count()
|
|
153
|
+
|
|
149
154
|
# Get basic model info
|
|
150
155
|
model_stats = {
|
|
151
156
|
"name": model._meta.verbose_name_plural
|
|
152
157
|
or model._meta.verbose_name
|
|
153
158
|
or model._meta.model_name,
|
|
154
|
-
"count":
|
|
159
|
+
"count": count,
|
|
155
160
|
"fields_count": len(model._meta.fields),
|
|
156
161
|
"admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
return model_stats
|
|
160
165
|
|
|
166
|
+
except (OperationalError, ProgrammingError):
|
|
167
|
+
# Table doesn't exist or other DB error - skip this model silently
|
|
168
|
+
return None
|
|
161
169
|
except Exception:
|
|
170
|
+
# Any other error - skip this model silently
|
|
162
171
|
return None
|
|
163
172
|
|
|
164
173
|
def get_stat_cards(self) -> List[Dict[str, Any]]:
|
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from typing import Any, Dict, List, Literal
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
14
15
|
|
|
@@ -105,7 +106,7 @@ class SystemHealthService:
|
|
|
105
106
|
|
|
106
107
|
def check_queue_health(self) -> Dict[str, Any]:
|
|
107
108
|
"""
|
|
108
|
-
Check task queue (
|
|
109
|
+
Check task queue (Django-RQ) health via Redis connection.
|
|
109
110
|
|
|
110
111
|
Returns:
|
|
111
112
|
Health status dictionary
|
|
@@ -191,90 +192,6 @@ class SystemHealthService:
|
|
|
191
192
|
'health_percentage': 0,
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
def check_django_q2_health(self) -> Dict[str, Any]:
|
|
195
|
-
"""
|
|
196
|
-
Check Django-Q2 task scheduling configuration and status.
|
|
197
|
-
|
|
198
|
-
Returns:
|
|
199
|
-
Health status dictionary with schedule count and cluster status
|
|
200
|
-
"""
|
|
201
|
-
try:
|
|
202
|
-
from django_cfg.core.config import get_current_config
|
|
203
|
-
|
|
204
|
-
config = get_current_config()
|
|
205
|
-
|
|
206
|
-
# Check if django_q2 is configured
|
|
207
|
-
if not hasattr(config, 'django_q2') or not config.django_q2:
|
|
208
|
-
return {
|
|
209
|
-
'component': 'django_q2',
|
|
210
|
-
'status': 'info',
|
|
211
|
-
'description': 'Django-Q2 scheduling not configured',
|
|
212
|
-
'last_check': datetime.now().isoformat(),
|
|
213
|
-
'health_percentage': 100,
|
|
214
|
-
'details': {
|
|
215
|
-
'enabled': False,
|
|
216
|
-
'schedules_count': 0,
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
django_q2_config = config.django_q2
|
|
221
|
-
|
|
222
|
-
# Check if enabled
|
|
223
|
-
if not django_q2_config.enabled:
|
|
224
|
-
return {
|
|
225
|
-
'component': 'django_q2',
|
|
226
|
-
'status': 'warning',
|
|
227
|
-
'description': 'Django-Q2 scheduling is disabled',
|
|
228
|
-
'last_check': datetime.now().isoformat(),
|
|
229
|
-
'health_percentage': 50,
|
|
230
|
-
'details': {
|
|
231
|
-
'enabled': False,
|
|
232
|
-
'schedules_count': len(django_q2_config.schedules) if django_q2_config.schedules else 0,
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
# Count schedules
|
|
237
|
-
schedules_count = len(django_q2_config.schedules) if django_q2_config.schedules else 0
|
|
238
|
-
|
|
239
|
-
# Try to check cluster status from database
|
|
240
|
-
cluster_running = False
|
|
241
|
-
try:
|
|
242
|
-
from django_q.models import Schedule, Task
|
|
243
|
-
from django.utils import timezone
|
|
244
|
-
from datetime import timedelta
|
|
245
|
-
|
|
246
|
-
# Check for recent task activity
|
|
247
|
-
recent_task = Task.objects.filter(
|
|
248
|
-
started__gte=timezone.now() - timedelta(minutes=5)
|
|
249
|
-
).exists()
|
|
250
|
-
cluster_running = recent_task
|
|
251
|
-
except Exception:
|
|
252
|
-
pass
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
'component': 'django_q2',
|
|
256
|
-
'status': 'healthy',
|
|
257
|
-
'description': f'{schedules_count} schedule(s) configured, cluster {"running" if cluster_running else "idle"}',
|
|
258
|
-
'last_check': datetime.now().isoformat(),
|
|
259
|
-
'health_percentage': 100,
|
|
260
|
-
'details': {
|
|
261
|
-
'enabled': True,
|
|
262
|
-
'schedules_count': schedules_count,
|
|
263
|
-
'cluster_running': cluster_running,
|
|
264
|
-
'workers': django_q2_config.workers,
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
except Exception as e:
|
|
269
|
-
self.logger.error(f"Django-Q2 health check failed: {e}")
|
|
270
|
-
return {
|
|
271
|
-
'component': 'django_q2',
|
|
272
|
-
'status': 'error',
|
|
273
|
-
'description': f'Django-Q2 check error: {str(e)}',
|
|
274
|
-
'last_check': datetime.now().isoformat(),
|
|
275
|
-
'health_percentage': 0,
|
|
276
|
-
}
|
|
277
|
-
|
|
278
195
|
def get_all_health_checks(self) -> List[Dict[str, Any]]:
|
|
279
196
|
"""
|
|
280
197
|
Run all health checks and return aggregated results.
|
|
@@ -289,7 +206,6 @@ class SystemHealthService:
|
|
|
289
206
|
self.check_cache_health(),
|
|
290
207
|
self.check_queue_health(),
|
|
291
208
|
self.check_storage_health(),
|
|
292
|
-
self.check_django_q2_health(),
|
|
293
209
|
]
|
|
294
210
|
|
|
295
211
|
return checks
|
|
@@ -328,42 +244,84 @@ class SystemHealthService:
|
|
|
328
244
|
Get quick action buttons for dashboard.
|
|
329
245
|
|
|
330
246
|
Returns:
|
|
331
|
-
List of quick action dictionaries
|
|
247
|
+
List of quick action dictionaries with safe URL resolution
|
|
332
248
|
|
|
333
|
-
%%AI_HINT: Actions link to admin pages or
|
|
249
|
+
%%AI_HINT: Actions link to admin pages or API endpoints%%
|
|
334
250
|
"""
|
|
251
|
+
from django.urls import reverse, NoReverseMatch
|
|
252
|
+
|
|
253
|
+
def safe_reverse(url_name: str, fallback: str) -> str:
|
|
254
|
+
"""Safely reverse URL with fallback."""
|
|
255
|
+
try:
|
|
256
|
+
return reverse(url_name)
|
|
257
|
+
except NoReverseMatch:
|
|
258
|
+
self.logger.debug(f"Could not reverse URL '{url_name}', using fallback: {fallback}")
|
|
259
|
+
return fallback
|
|
260
|
+
|
|
335
261
|
actions = [
|
|
262
|
+
{
|
|
263
|
+
'title': 'Admin Panel',
|
|
264
|
+
'description': 'Open Django admin interface',
|
|
265
|
+
'icon': 'admin_panel_settings',
|
|
266
|
+
'link': safe_reverse('admin:index', '/admin/'),
|
|
267
|
+
'color': 'primary',
|
|
268
|
+
'category': 'admin',
|
|
269
|
+
},
|
|
336
270
|
{
|
|
337
271
|
'title': 'User Management',
|
|
338
272
|
'description': 'Manage users and permissions',
|
|
339
273
|
'icon': 'people',
|
|
340
|
-
'link': '/admin/auth/user/',
|
|
274
|
+
'link': safe_reverse('admin:auth_user_changelist', '/admin/auth/user/'),
|
|
341
275
|
'color': 'primary',
|
|
342
276
|
'category': 'admin',
|
|
343
277
|
},
|
|
344
278
|
{
|
|
345
|
-
'title': '
|
|
346
|
-
'description': '
|
|
347
|
-
'icon': '
|
|
348
|
-
'link': '/admin/
|
|
279
|
+
'title': 'Settings',
|
|
280
|
+
'description': 'Configure application settings',
|
|
281
|
+
'icon': 'settings',
|
|
282
|
+
'link': safe_reverse('admin:constance_config_changelist', '/admin/constance/config/'),
|
|
283
|
+
'color': 'default',
|
|
284
|
+
'category': 'admin',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
'title': 'System Health',
|
|
288
|
+
'description': 'Check system health status',
|
|
289
|
+
'icon': 'favorite',
|
|
290
|
+
'link': '/cfg/dashboard/api/system/health/',
|
|
291
|
+
'color': 'success',
|
|
292
|
+
'category': 'monitoring',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
'title': 'RQ Workers',
|
|
296
|
+
'description': 'Monitor task queue workers',
|
|
297
|
+
'icon': 'work',
|
|
298
|
+
'link': '/cfg/rq/workers/',
|
|
299
|
+
'color': 'info',
|
|
300
|
+
'category': 'monitoring',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
'title': 'RQ Jobs',
|
|
304
|
+
'description': 'View and manage background jobs',
|
|
305
|
+
'icon': 'assignment',
|
|
306
|
+
'link': '/cfg/rq/jobs/',
|
|
349
307
|
'color': 'secondary',
|
|
350
|
-
'category': '
|
|
308
|
+
'category': 'monitoring',
|
|
351
309
|
},
|
|
352
310
|
{
|
|
353
|
-
'title': '
|
|
354
|
-
'description': '
|
|
355
|
-
'icon': '
|
|
356
|
-
'link': '/cfg/
|
|
357
|
-
'color': '
|
|
358
|
-
'category': '
|
|
311
|
+
'title': 'RQ Schedules',
|
|
312
|
+
'description': 'Manage scheduled tasks',
|
|
313
|
+
'icon': 'schedule',
|
|
314
|
+
'link': '/cfg/rq/schedules/',
|
|
315
|
+
'color': 'info',
|
|
316
|
+
'category': 'monitoring',
|
|
359
317
|
},
|
|
360
318
|
{
|
|
361
|
-
'title': '
|
|
362
|
-
'description': '
|
|
363
|
-
'icon': '
|
|
364
|
-
'link': '/cfg/
|
|
365
|
-
'color': '
|
|
366
|
-
'category': '
|
|
319
|
+
'title': 'Commands',
|
|
320
|
+
'description': 'Execute Django management commands',
|
|
321
|
+
'icon': 'terminal',
|
|
322
|
+
'link': '/cfg/dashboard/api/commands/',
|
|
323
|
+
'color': 'warning',
|
|
324
|
+
'category': 'admin',
|
|
367
325
|
},
|
|
368
326
|
]
|
|
369
327
|
|
|
@@ -21,7 +21,6 @@ from .views import (
|
|
|
21
21
|
ChartsViewSet,
|
|
22
22
|
CommandsViewSet,
|
|
23
23
|
APIZonesViewSet,
|
|
24
|
-
DjangoQ2ViewSet,
|
|
25
24
|
)
|
|
26
25
|
|
|
27
26
|
app_name = 'django_cfg_dashboard'
|
|
@@ -35,7 +34,6 @@ router.register(r'activity', ActivityViewSet, basename='activity')
|
|
|
35
34
|
router.register(r'charts', ChartsViewSet, basename='charts')
|
|
36
35
|
router.register(r'commands', CommandsViewSet, basename='commands')
|
|
37
36
|
router.register(r'zones', APIZonesViewSet, basename='zones')
|
|
38
|
-
router.register(r'django_q2', DjangoQ2ViewSet, basename='django_q2')
|
|
39
37
|
|
|
40
38
|
urlpatterns = [
|
|
41
39
|
# RESTful API endpoints using ViewSets
|
|
@@ -11,7 +11,6 @@ from .activity_views import ActivityViewSet
|
|
|
11
11
|
from .charts_views import ChartsViewSet
|
|
12
12
|
from .commands_views import CommandsViewSet
|
|
13
13
|
from .apizones_views import APIZonesViewSet
|
|
14
|
-
from .django_q2_views import DjangoQ2ViewSet
|
|
15
14
|
|
|
16
15
|
__all__ = [
|
|
17
16
|
'OverviewViewSet',
|
|
@@ -21,5 +20,4 @@ __all__ = [
|
|
|
21
20
|
'ChartsViewSet',
|
|
22
21
|
'CommandsViewSet',
|
|
23
22
|
'APIZonesViewSet',
|
|
24
|
-
'DjangoQ2ViewSet',
|
|
25
23
|
]
|
|
@@ -12,14 +12,12 @@ import json
|
|
|
12
12
|
import logging
|
|
13
13
|
|
|
14
14
|
from django.http import StreamingHttpResponse
|
|
15
|
+
from django_cfg.mixins import SuperAdminAPIMixin
|
|
15
16
|
from drf_spectacular.utils import extend_schema
|
|
16
17
|
from rest_framework import status, viewsets
|
|
17
|
-
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
|
|
18
18
|
from rest_framework.decorators import action
|
|
19
19
|
from rest_framework.response import Response
|
|
20
|
-
from rest_framework_simplejwt.authentication import JWTAuthentication
|
|
21
20
|
|
|
22
|
-
from ..permissions import IsSuperAdmin
|
|
23
21
|
from ..services import CommandsService
|
|
24
22
|
from ..serializers import (
|
|
25
23
|
CommandSerializer,
|
|
@@ -31,15 +29,14 @@ from ..serializers import (
|
|
|
31
29
|
logger = logging.getLogger(__name__)
|
|
32
30
|
|
|
33
31
|
|
|
34
|
-
class CommandsViewSet(viewsets.GenericViewSet):
|
|
32
|
+
class CommandsViewSet(SuperAdminAPIMixin, viewsets.GenericViewSet):
|
|
35
33
|
"""
|
|
36
34
|
Commands ViewSet
|
|
37
35
|
|
|
38
36
|
Provides endpoints for Django management commands discovery.
|
|
37
|
+
Requires superuser privileges for all operations.
|
|
39
38
|
"""
|
|
40
39
|
|
|
41
|
-
authentication_classes = [JWTAuthentication, SessionAuthentication, BasicAuthentication]
|
|
42
|
-
permission_classes = [IsSuperAdmin] # Only superusers can access commands
|
|
43
40
|
serializer_class = CommandSerializer
|
|
44
41
|
pagination_class = None # Disable pagination for commands list
|
|
45
42
|
|