django-cfg 1.5.1__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 +1 -1
- 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/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +2 -3
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- 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 +6 -7
- django_cfg/core/base/config_model.py +10 -26
- django_cfg/core/builders/apps_builder.py +4 -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/orchestrator.py +9 -19
- 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/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 +4 -6
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- django_cfg/modules/django_admin/utils/html/composition.py +9 -2
- django_cfg/modules/django_unfold/navigation.py +1 -26
- django_cfg/pyproject.toml +4 -4
- django_cfg/registry/core.py +4 -7
- 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.5.1.dist-info → django_cfg-1.5.2.dist-info}/METADATA +5 -6
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.2.dist-info}/RECORD +77 -82
- 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/config.py +0 -98
- django_cfg/apps/tasks/admin/task_log.py +0 -238
- 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/migrations/__init__.py +0 -0
- 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_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/__init__.py +0 -0
- 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-1.5.1.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.1.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -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
|
|
|
@@ -8,6 +8,7 @@ Endpoint for complete dashboard overview:
|
|
|
8
8
|
import logging
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
|
|
11
|
+
from django.db import transaction
|
|
11
12
|
from drf_spectacular.utils import extend_schema
|
|
12
13
|
from rest_framework import status, viewsets
|
|
13
14
|
|
|
@@ -32,6 +33,11 @@ class OverviewViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
|
32
33
|
|
|
33
34
|
serializer_class = DashboardOverviewSerializer
|
|
34
35
|
|
|
36
|
+
@transaction.non_atomic_requests
|
|
37
|
+
def dispatch(self, request, *args, **kwargs):
|
|
38
|
+
"""Disable atomic requests for this viewset."""
|
|
39
|
+
return super().dispatch(request, *args, **kwargs)
|
|
40
|
+
|
|
35
41
|
@extend_schema(
|
|
36
42
|
summary="Get dashboard overview",
|
|
37
43
|
description="Retrieve complete dashboard data including stats, health, actions, and metrics",
|
|
@@ -40,24 +46,19 @@ class OverviewViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
|
40
46
|
)
|
|
41
47
|
@action(detail=False, methods=['get'], url_path='', url_name='overview')
|
|
42
48
|
def overview(self, request):
|
|
43
|
-
"""
|
|
44
|
-
Get complete dashboard overview.
|
|
45
|
-
|
|
46
|
-
Returns all dashboard data in a single request:
|
|
47
|
-
- Statistics cards
|
|
48
|
-
- System health status
|
|
49
|
-
- Quick actions
|
|
50
|
-
- Recent activity
|
|
51
|
-
- System metrics
|
|
52
|
-
- User statistics
|
|
53
|
-
"""
|
|
49
|
+
"""Get complete dashboard overview."""
|
|
54
50
|
try:
|
|
55
51
|
stats_service = StatisticsService()
|
|
56
52
|
health_service = SystemHealthService()
|
|
57
53
|
charts_service = ChartsService()
|
|
58
54
|
|
|
59
|
-
# Get app statistics
|
|
60
|
-
|
|
55
|
+
# Get app statistics - wrapped in try/except since it queries all models
|
|
56
|
+
try:
|
|
57
|
+
app_stats_dict = stats_service.get_app_statistics()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Error getting app stats: {e}")
|
|
60
|
+
app_stats_dict = {'apps': {}}
|
|
61
|
+
|
|
61
62
|
app_statistics_list = [
|
|
62
63
|
{
|
|
63
64
|
'app_name': app_label,
|
django_cfg/apps/knowbase/apps.py
CHANGED
|
@@ -21,8 +21,8 @@ class KnowbaseConfig(AppConfig):
|
|
|
21
21
|
# Connect post-migrate signal for database setup
|
|
22
22
|
post_migrate.connect(self.create_pgvector_extension, sender=self)
|
|
23
23
|
|
|
24
|
-
# Note: Task system initialization removed -
|
|
25
|
-
#
|
|
24
|
+
# Note: Task system initialization removed - tasks are handled by Django-RQ
|
|
25
|
+
# Background tasks can be scheduled using django_rq.enqueue()
|
|
26
26
|
|
|
27
27
|
def create_pgvector_extension(self, sender, **kwargs):
|
|
28
28
|
"""Create pgvector extension and indexes if they don't exist."""
|
|
@@ -184,10 +184,9 @@ class CloudflareApiKeyAdmin(PydanticAdmin):
|
|
|
184
184
|
]
|
|
185
185
|
|
|
186
186
|
total_count = obj.cloudflaresite_set.count()
|
|
187
|
-
if total_count > 10
|
|
188
|
-
site_items.append(f"... and {total_count - 10} more sites")
|
|
187
|
+
overflow_item = [f"... and {total_count - 10} more sites"] if total_count > 10 else []
|
|
189
188
|
|
|
190
|
-
return "\n".join(site_items)
|
|
189
|
+
return "\n".join(site_items + overflow_item)
|
|
191
190
|
sites_using_key.short_description = "Sites Using This Key"
|
|
192
191
|
|
|
193
192
|
def changelist_view(self, request, extra_context=None):
|
|
@@ -243,19 +243,20 @@ class EmailLogAdmin(PydanticAdmin):
|
|
|
243
243
|
@computed_field("Tracking")
|
|
244
244
|
def tracking_display(self, obj: EmailLog) -> str:
|
|
245
245
|
"""Display tracking status with badges."""
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
246
|
+
# Declarative approach - no imperative .append()
|
|
247
|
+
opened_badge = (
|
|
248
|
+
self.html.badge("Opened", variant="success", icon=Icons.VISIBILITY)
|
|
249
|
+
if obj.is_opened else
|
|
250
|
+
self.html.badge("Not Opened", variant="secondary", icon=Icons.VISIBILITY_OFF)
|
|
251
|
+
)
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
clicked_badge = (
|
|
254
|
+
self.html.badge("Clicked", variant="info", icon=Icons.MOUSE)
|
|
255
|
+
if obj.is_clicked else
|
|
256
|
+
self.html.badge("Not Clicked", variant="secondary", icon=Icons.TOUCH_APP)
|
|
257
|
+
)
|
|
257
258
|
|
|
258
|
-
return " | "
|
|
259
|
+
return self.html.inline(opened_badge, clicked_badge, separator=" | ")
|
|
259
260
|
|
|
260
261
|
|
|
261
262
|
# ===== Newsletter Admin Config =====
|