django-cfg 1.4.58__py3-none-any.whl → 1.4.60__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/ipc/RPC_LOGGING.md +321 -0
- django_cfg/apps/ipc/TESTING.md +539 -0
- django_cfg/apps/ipc/__init__.py +12 -3
- django_cfg/apps/ipc/admin.py +212 -0
- django_cfg/apps/ipc/migrations/0001_initial.py +137 -0
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +221 -0
- django_cfg/apps/ipc/serializers/__init__.py +10 -0
- django_cfg/apps/ipc/serializers/serializers.py +114 -0
- django_cfg/apps/ipc/services/client/client.py +83 -4
- django_cfg/apps/ipc/services/logging.py +239 -0
- django_cfg/apps/ipc/services/monitor.py +5 -3
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +269 -0
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +259 -0
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +375 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +22 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +9 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +9 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +23 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/stat_cards.html +50 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +47 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/tab_navigation.html +29 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +184 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +56 -0
- django_cfg/apps/ipc/urls.py +4 -2
- django_cfg/apps/ipc/views/__init__.py +7 -2
- django_cfg/apps/ipc/views/dashboard.py +1 -1
- django_cfg/apps/ipc/views/{viewsets.py → monitoring.py} +17 -11
- django_cfg/apps/ipc/views/testing.py +285 -0
- django_cfg/core/backends/__init__.py +1 -0
- django_cfg/core/backends/smtp.py +69 -0
- django_cfg/middleware/authentication.py +157 -0
- django_cfg/models/api/drf/config.py +2 -2
- django_cfg/models/services/email.py +11 -1
- django_cfg/modules/django_client/system/generate_mjs_clients.py +1 -1
- django_cfg/modules/django_dashboard/sections/widgets.py +209 -0
- django_cfg/modules/django_unfold/callbacks/main.py +43 -18
- django_cfg/modules/django_unfold/dashboard.py +41 -4
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/js/api/index.mjs +8 -3
- django_cfg/static/js/api/ipc/client.mjs +40 -0
- django_cfg/static/js/api/knowbase/client.mjs +309 -0
- django_cfg/static/js/api/knowbase/index.mjs +13 -0
- django_cfg/static/js/api/payments/client.mjs +46 -1215
- django_cfg/static/js/api/types.mjs +164 -337
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +13 -1
- django_cfg/templates/admin/sections/widgets_section.html +129 -0
- django_cfg/templates/admin/snippets/tabs/widgets_tab.html +38 -0
- django_cfg/utils/smart_defaults.py +1 -1
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/METADATA +1 -1
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/RECORD +58 -31
- django_cfg/apps/ipc/templates/django_cfg_ipc/dashboard.html +0 -202
- /django_cfg/apps/ipc/static/django_cfg_ipc/js/{dashboard.mjs → dashboard.mjs.old} +0 -0
- /django_cfg/apps/ipc/templates/django_cfg_ipc/{base.html → layout/base.html} +0 -0
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom SMTP Email Backend for Django CFG.
|
|
3
|
+
|
|
4
|
+
Supports SMTP with unverified SSL certificates for development environments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import smtplib
|
|
8
|
+
import ssl
|
|
9
|
+
|
|
10
|
+
from django.core.mail.backends.smtp import EmailBackend as DjangoSMTPBackend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnverifiedSSLEmailBackend(DjangoSMTPBackend):
|
|
14
|
+
"""
|
|
15
|
+
SMTP backend that accepts self-signed SSL certificates.
|
|
16
|
+
|
|
17
|
+
WARNING: Only use in development! In production, use proper SSL certificates
|
|
18
|
+
with the standard Django SMTP backend.
|
|
19
|
+
|
|
20
|
+
This backend disables SSL certificate verification, which makes it vulnerable
|
|
21
|
+
to man-in-the-middle attacks. It's designed for dev environments where you
|
|
22
|
+
control the mail server and use self-signed certificates.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def open(self):
|
|
26
|
+
"""
|
|
27
|
+
Open connection with unverified SSL context.
|
|
28
|
+
|
|
29
|
+
Overrides parent to use unverified SSL context for self-signed certs.
|
|
30
|
+
"""
|
|
31
|
+
if self.connection:
|
|
32
|
+
# Already have a connection
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
connection_params = {'timeout': self.timeout}
|
|
36
|
+
if self.ssl_keyfile:
|
|
37
|
+
connection_params['keyfile'] = self.ssl_keyfile
|
|
38
|
+
if self.ssl_certfile:
|
|
39
|
+
connection_params['certfile'] = self.ssl_certfile
|
|
40
|
+
|
|
41
|
+
if self.use_ssl:
|
|
42
|
+
# Create unverified SSL context (accepts self-signed certs)
|
|
43
|
+
ssl_context = ssl.create_default_context()
|
|
44
|
+
ssl_context.check_hostname = False
|
|
45
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
46
|
+
|
|
47
|
+
connection_params['context'] = ssl_context
|
|
48
|
+
self.connection = smtplib.SMTP_SSL(
|
|
49
|
+
self.host, self.port, **connection_params
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
self.connection = smtplib.SMTP(
|
|
53
|
+
self.host, self.port, **connection_params
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if self.use_tls:
|
|
57
|
+
# Create unverified SSL context for STARTTLS too
|
|
58
|
+
ssl_context = ssl.create_default_context()
|
|
59
|
+
ssl_context.check_hostname = False
|
|
60
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
61
|
+
|
|
62
|
+
self.connection.ehlo()
|
|
63
|
+
self.connection.starttls(context=ssl_context)
|
|
64
|
+
self.connection.ehlo()
|
|
65
|
+
|
|
66
|
+
if self.username and self.password:
|
|
67
|
+
self.connection.login(self.username, self.password)
|
|
68
|
+
|
|
69
|
+
return True
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom JWT Authentication for Django CFG.
|
|
3
|
+
|
|
4
|
+
Extends rest_framework_simplejwt.authentication.JWTAuthentication to automatically
|
|
5
|
+
update user's last_login field on successful authentication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from django.contrib.auth import get_user_model
|
|
11
|
+
from django.utils import timezone
|
|
12
|
+
from rest_framework_simplejwt.authentication import JWTAuthentication
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
User = get_user_model()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JWTAuthenticationWithLastLogin(JWTAuthentication):
|
|
20
|
+
"""
|
|
21
|
+
JWT Authentication that updates last_login on successful authentication.
|
|
22
|
+
|
|
23
|
+
Updates last_login field with intelligent throttling to avoid database spam.
|
|
24
|
+
Only updates if last_login is None or older than the configured interval.
|
|
25
|
+
|
|
26
|
+
Features:
|
|
27
|
+
- Automatic last_login tracking for all JWT-authenticated requests
|
|
28
|
+
- Built-in throttling (default: 5 minutes) to minimize database writes
|
|
29
|
+
- In-memory cache for tracking last update times
|
|
30
|
+
- Automatic cache cleanup to prevent memory leaks
|
|
31
|
+
- Error handling to prevent authentication failures
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
Add to REST_FRAMEWORK settings:
|
|
35
|
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
36
|
+
'django_cfg.apps.accounts.authentication.JWTAuthenticationWithLastLogin',
|
|
37
|
+
]
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Class-level cache to track last update times (shared across all instances)
|
|
41
|
+
_last_updates = {}
|
|
42
|
+
|
|
43
|
+
# Update interval in seconds (5 minutes by default, same as UserActivityMiddleware)
|
|
44
|
+
UPDATE_INTERVAL = 300
|
|
45
|
+
|
|
46
|
+
# Maximum cache size before cleanup (prevents memory leaks)
|
|
47
|
+
MAX_CACHE_SIZE = 1000
|
|
48
|
+
CLEANUP_CACHE_SIZE = 500
|
|
49
|
+
|
|
50
|
+
def authenticate(self, request):
|
|
51
|
+
"""
|
|
52
|
+
Authenticate request and update last_login if needed.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
request: Django HttpRequest object
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Tuple of (user, token) if authentication succeeds, None otherwise
|
|
59
|
+
"""
|
|
60
|
+
# Perform standard JWT authentication
|
|
61
|
+
result = super().authenticate(request)
|
|
62
|
+
|
|
63
|
+
if result is not None:
|
|
64
|
+
user, token = result
|
|
65
|
+
# Update last_login with throttling
|
|
66
|
+
self._update_last_login(user)
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
def _update_last_login(self, user):
|
|
71
|
+
"""
|
|
72
|
+
Update user's last_login field with intelligent throttling.
|
|
73
|
+
|
|
74
|
+
Only updates if:
|
|
75
|
+
- last_login is None (never logged in)
|
|
76
|
+
- last_login is older than UPDATE_INTERVAL seconds
|
|
77
|
+
|
|
78
|
+
Uses UPDATE query to avoid triggering signals and save() overhead.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
user: User instance to update
|
|
82
|
+
"""
|
|
83
|
+
now = timezone.now()
|
|
84
|
+
user_id = user.pk
|
|
85
|
+
|
|
86
|
+
# Check if we should update (avoid database spam)
|
|
87
|
+
last_update = self._last_updates.get(user_id)
|
|
88
|
+
if last_update and (now - last_update).total_seconds() < self.UPDATE_INTERVAL:
|
|
89
|
+
# Skip update - too soon since last update
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Use update() to avoid triggering signals and save() overhead
|
|
94
|
+
# This is more efficient than user.save(update_fields=['last_login'])
|
|
95
|
+
updated_count = User.objects.filter(pk=user_id).update(last_login=now)
|
|
96
|
+
|
|
97
|
+
if updated_count > 0:
|
|
98
|
+
# Cache the update time
|
|
99
|
+
self._last_updates[user_id] = now
|
|
100
|
+
|
|
101
|
+
logger.debug(
|
|
102
|
+
f"Updated last_login for user {user.email} (ID: {user_id}) "
|
|
103
|
+
f"via JWT authentication"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Clean up old cache entries to prevent memory leaks
|
|
107
|
+
if len(self._last_updates) > self.MAX_CACHE_SIZE:
|
|
108
|
+
self._cleanup_cache()
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
# Log error but don't break authentication
|
|
112
|
+
# Authentication should succeed even if last_login update fails
|
|
113
|
+
logger.error(
|
|
114
|
+
f"Failed to update last_login for user {user_id}: {e}",
|
|
115
|
+
exc_info=True
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _cleanup_cache(self):
|
|
119
|
+
"""
|
|
120
|
+
Clean up old cache entries to prevent memory leaks.
|
|
121
|
+
|
|
122
|
+
Keeps only the most recent CLEANUP_CACHE_SIZE entries.
|
|
123
|
+
Sorted by update time (newest first).
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
# Sort by update time (value), keep newest entries
|
|
127
|
+
sorted_items = sorted(
|
|
128
|
+
self._last_updates.items(),
|
|
129
|
+
key=lambda x: x[1],
|
|
130
|
+
reverse=True
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Keep only the most recent entries
|
|
134
|
+
self._last_updates = dict(sorted_items[:self.CLEANUP_CACHE_SIZE])
|
|
135
|
+
|
|
136
|
+
logger.debug(
|
|
137
|
+
f"Cleaned up last_login cache: "
|
|
138
|
+
f"kept {len(self._last_updates)} most recent entries"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Failed to cleanup last_login cache: {e}")
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def get_cache_stats(cls):
|
|
146
|
+
"""
|
|
147
|
+
Get cache statistics for monitoring/debugging.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
dict: Cache statistics including size and configuration
|
|
151
|
+
"""
|
|
152
|
+
return {
|
|
153
|
+
'cache_size': len(cls._last_updates),
|
|
154
|
+
'max_cache_size': cls.MAX_CACHE_SIZE,
|
|
155
|
+
'update_interval_seconds': cls.UPDATE_INTERVAL,
|
|
156
|
+
'update_interval_minutes': cls.UPDATE_INTERVAL / 60,
|
|
157
|
+
}
|
|
@@ -17,12 +17,12 @@ class DRFConfig(BaseModel):
|
|
|
17
17
|
# Authentication
|
|
18
18
|
authentication_classes: List[str] = Field(
|
|
19
19
|
default_factory=lambda: [
|
|
20
|
-
'
|
|
20
|
+
'django_cfg.middleware.authentication.JWTAuthenticationWithLastLogin',
|
|
21
21
|
'rest_framework.authentication.TokenAuthentication',
|
|
22
22
|
# SessionAuthentication removed from defaults (requires CSRF)
|
|
23
23
|
# Add it manually in your config if you need Browsable API with session auth
|
|
24
24
|
],
|
|
25
|
-
description="Default authentication classes"
|
|
25
|
+
description="Default authentication classes (JWT with auto last_login update)"
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
# Permissions
|
|
@@ -58,6 +58,11 @@ class EmailConfig(BaseConfig):
|
|
|
58
58
|
description="Use SSL for SMTP"
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
+
ssl_verify: bool = Field(
|
|
62
|
+
default=True,
|
|
63
|
+
description="Verify SSL certificates (set False for self-signed certs in dev)"
|
|
64
|
+
)
|
|
65
|
+
|
|
61
66
|
timeout: int = Field(
|
|
62
67
|
default=30,
|
|
63
68
|
ge=1,
|
|
@@ -111,7 +116,12 @@ class EmailConfig(BaseConfig):
|
|
|
111
116
|
def to_django_settings(self) -> Dict[str, Any]:
|
|
112
117
|
"""Convert to Django email settings."""
|
|
113
118
|
if self.backend == 'smtp':
|
|
114
|
-
|
|
119
|
+
# Choose backend based on SSL verification setting
|
|
120
|
+
if self.ssl_verify:
|
|
121
|
+
backend_class = 'django.core.mail.backends.smtp.EmailBackend'
|
|
122
|
+
else:
|
|
123
|
+
# Use custom backend that accepts self-signed SSL certs
|
|
124
|
+
backend_class = 'django_cfg.core.backends.smtp.UnverifiedSSLEmailBackend'
|
|
115
125
|
|
|
116
126
|
settings = {
|
|
117
127
|
'EMAIL_BACKEND': backend_class,
|
|
@@ -54,7 +54,7 @@ def main():
|
|
|
54
54
|
django_cfg_dir = modules_dir.parent # django_cfg
|
|
55
55
|
src_dir = django_cfg_dir.parent # src
|
|
56
56
|
dev_dir = src_dir.parent # django-cfg-dev
|
|
57
|
-
example_django_dir = dev_dir.parent / "
|
|
57
|
+
example_django_dir = dev_dir.parent / "solution" / "projects" / "django"
|
|
58
58
|
|
|
59
59
|
# Output directory for MJS clients
|
|
60
60
|
if args.output:
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Widgets section for dashboard.
|
|
2
|
+
|
|
3
|
+
Automatically renders widgets from DashboardManager.get_widgets_config()
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
import psutil
|
|
10
|
+
from django.db.models import Avg
|
|
11
|
+
from django.template import Context, Template
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
|
|
14
|
+
from .base import DataSection
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WidgetsSection(DataSection):
|
|
18
|
+
"""
|
|
19
|
+
Widgets section showing automatically generated dashboard widgets.
|
|
20
|
+
|
|
21
|
+
Widgets are defined in DashboardManager.get_widgets_config() and
|
|
22
|
+
can include:
|
|
23
|
+
- System metrics (CPU, Memory, Disk)
|
|
24
|
+
- RPC monitoring stats
|
|
25
|
+
- Custom application widgets
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
template_name = "admin/sections/widgets_section.html"
|
|
29
|
+
title = "Dashboard Widgets"
|
|
30
|
+
icon = "widgets"
|
|
31
|
+
|
|
32
|
+
def get_data(self) -> Dict[str, Any]:
|
|
33
|
+
"""Get widgets configuration from DashboardManager."""
|
|
34
|
+
from django_cfg.modules.django_unfold.dashboard import get_dashboard_manager
|
|
35
|
+
|
|
36
|
+
dashboard_manager = get_dashboard_manager()
|
|
37
|
+
|
|
38
|
+
# Get widgets from dashboard manager (base system widgets)
|
|
39
|
+
widgets = dashboard_manager.get_widgets_config()
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
'widgets': widgets,
|
|
43
|
+
'widgets_count': len(widgets),
|
|
44
|
+
'has_widgets': len(widgets) > 0,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def merge_custom_widgets(self, widgets: List[Dict], custom_widgets: List[Any]) -> List[Dict]:
|
|
48
|
+
"""
|
|
49
|
+
Merge custom widgets from dashboard_callback.
|
|
50
|
+
|
|
51
|
+
Allows projects to add widgets via dashboard_callback:
|
|
52
|
+
context["custom_widgets"] = [
|
|
53
|
+
StatsCardsWidget(...),
|
|
54
|
+
...
|
|
55
|
+
]
|
|
56
|
+
"""
|
|
57
|
+
if not custom_widgets:
|
|
58
|
+
return widgets
|
|
59
|
+
|
|
60
|
+
# Convert custom widgets to dicts if they are Pydantic models
|
|
61
|
+
for widget in custom_widgets:
|
|
62
|
+
if hasattr(widget, 'to_dict'):
|
|
63
|
+
widgets.append(widget.to_dict())
|
|
64
|
+
elif hasattr(widget, 'model_dump'):
|
|
65
|
+
widgets.append(widget.model_dump())
|
|
66
|
+
elif isinstance(widget, dict):
|
|
67
|
+
widgets.append(widget)
|
|
68
|
+
|
|
69
|
+
return widgets
|
|
70
|
+
|
|
71
|
+
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
|
72
|
+
"""Add additional context for widget rendering."""
|
|
73
|
+
context = super().get_context_data(**kwargs)
|
|
74
|
+
|
|
75
|
+
# Get base widgets from DashboardManager
|
|
76
|
+
widgets = context['data']['widgets']
|
|
77
|
+
|
|
78
|
+
# Merge custom widgets from dashboard_callback if provided
|
|
79
|
+
custom_widgets_from_callback = kwargs.get('custom_widgets', [])
|
|
80
|
+
if custom_widgets_from_callback:
|
|
81
|
+
widgets = self.merge_custom_widgets(widgets, custom_widgets_from_callback)
|
|
82
|
+
# Update count
|
|
83
|
+
context['data']['widgets_count'] = len(widgets)
|
|
84
|
+
context['data']['has_widgets'] = len(widgets) > 0
|
|
85
|
+
|
|
86
|
+
# Get metrics data first
|
|
87
|
+
metrics_data = {}
|
|
88
|
+
metrics_data.update(self.get_system_metrics())
|
|
89
|
+
|
|
90
|
+
# Add RPC metrics if enabled
|
|
91
|
+
dashboard_manager = self._get_dashboard_manager()
|
|
92
|
+
if dashboard_manager.is_rpc_enabled():
|
|
93
|
+
metrics_data.update(self.get_rpc_metrics())
|
|
94
|
+
|
|
95
|
+
# Also merge any custom metrics from kwargs
|
|
96
|
+
custom_metrics = kwargs.get('custom_metrics', {})
|
|
97
|
+
if custom_metrics:
|
|
98
|
+
metrics_data.update(custom_metrics)
|
|
99
|
+
|
|
100
|
+
# Process widgets and resolve template variables
|
|
101
|
+
processed_stats_widgets = []
|
|
102
|
+
for widget in widgets:
|
|
103
|
+
if widget.get('type') == 'stats_cards':
|
|
104
|
+
processed_widget = self._process_stats_widget(widget, metrics_data)
|
|
105
|
+
processed_stats_widgets.append(processed_widget)
|
|
106
|
+
|
|
107
|
+
chart_widgets = [w for w in widgets if w.get('type') == 'chart']
|
|
108
|
+
custom_widgets = [w for w in widgets if w.get('type') not in ['stats_cards', 'chart']]
|
|
109
|
+
|
|
110
|
+
# Add processed widgets
|
|
111
|
+
context.update({
|
|
112
|
+
'stats_widgets': processed_stats_widgets,
|
|
113
|
+
'chart_widgets': chart_widgets,
|
|
114
|
+
'custom_widgets': custom_widgets,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
# Also add metrics for direct access
|
|
118
|
+
context.update(metrics_data)
|
|
119
|
+
|
|
120
|
+
return context
|
|
121
|
+
|
|
122
|
+
def _get_dashboard_manager(self):
|
|
123
|
+
"""Get dashboard manager instance (lazy import to avoid circular dependencies)."""
|
|
124
|
+
from django_cfg.modules.django_unfold.dashboard import get_dashboard_manager
|
|
125
|
+
return get_dashboard_manager()
|
|
126
|
+
|
|
127
|
+
def _process_stats_widget(self, widget: Dict[str, Any], context_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
128
|
+
"""Process StatsCardsWidget and resolve template variables in cards."""
|
|
129
|
+
processed_widget = widget.copy()
|
|
130
|
+
processed_cards = []
|
|
131
|
+
|
|
132
|
+
for card in widget.get('cards', []):
|
|
133
|
+
processed_card = card.copy()
|
|
134
|
+
|
|
135
|
+
# Resolve value_template using Django template engine
|
|
136
|
+
value_template = card.get('value_template', '')
|
|
137
|
+
if '{{' in value_template:
|
|
138
|
+
try:
|
|
139
|
+
template = Template(value_template)
|
|
140
|
+
context = Context(context_data)
|
|
141
|
+
resolved_value = template.render(context)
|
|
142
|
+
processed_card['value_template'] = resolved_value
|
|
143
|
+
except Exception:
|
|
144
|
+
# Keep original if rendering fails
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# Also resolve change field if it has template variables
|
|
148
|
+
change_template = card.get('change', '')
|
|
149
|
+
if change_template and '{{' in change_template:
|
|
150
|
+
try:
|
|
151
|
+
template = Template(change_template)
|
|
152
|
+
context = Context(context_data)
|
|
153
|
+
resolved_change = template.render(context)
|
|
154
|
+
processed_card['change'] = resolved_change
|
|
155
|
+
except Exception:
|
|
156
|
+
# Keep original if rendering fails
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
processed_cards.append(processed_card)
|
|
160
|
+
|
|
161
|
+
processed_widget['cards'] = processed_cards
|
|
162
|
+
return processed_widget
|
|
163
|
+
|
|
164
|
+
def get_system_metrics(self) -> Dict[str, Any]:
|
|
165
|
+
"""Get system metrics for widgets."""
|
|
166
|
+
return {
|
|
167
|
+
'cpu_percent': round(psutil.cpu_percent(interval=0.1), 1),
|
|
168
|
+
'memory_percent': round(psutil.virtual_memory().percent, 1),
|
|
169
|
+
'disk_percent': round(psutil.disk_usage('/').percent, 1),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def get_rpc_metrics(self) -> Dict[str, Any]:
|
|
173
|
+
"""Get RPC metrics for widgets."""
|
|
174
|
+
try:
|
|
175
|
+
from django_cfg.apps.ipc.models import RPCLog
|
|
176
|
+
|
|
177
|
+
# Get stats for last 24 hours
|
|
178
|
+
since = timezone.now() - timedelta(hours=24)
|
|
179
|
+
|
|
180
|
+
logs = RPCLog.objects.filter(timestamp__gte=since)
|
|
181
|
+
|
|
182
|
+
total_calls = logs.count()
|
|
183
|
+
successful_calls = logs.filter(status='success').count()
|
|
184
|
+
failed_calls = logs.filter(status='error').count()
|
|
185
|
+
|
|
186
|
+
success_rate = round((successful_calls / total_calls * 100) if total_calls > 0 else 0, 1)
|
|
187
|
+
|
|
188
|
+
avg_duration = logs.filter(
|
|
189
|
+
duration__isnull=False
|
|
190
|
+
).aggregate(
|
|
191
|
+
avg=Avg('duration')
|
|
192
|
+
)['avg']
|
|
193
|
+
|
|
194
|
+
avg_duration = round(avg_duration * 1000, 1) if avg_duration else 0 # Convert to ms
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
'rpc_total_calls': total_calls,
|
|
198
|
+
'rpc_success_rate': success_rate,
|
|
199
|
+
'rpc_avg_duration': avg_duration,
|
|
200
|
+
'rpc_failed_calls': failed_calls,
|
|
201
|
+
}
|
|
202
|
+
except Exception as e:
|
|
203
|
+
# Return zeros if RPC models not available
|
|
204
|
+
return {
|
|
205
|
+
'rpc_total_calls': 0,
|
|
206
|
+
'rpc_success_rate': 0,
|
|
207
|
+
'rpc_avg_duration': 0,
|
|
208
|
+
'rpc_failed_calls': 0,
|
|
209
|
+
}
|
|
@@ -19,6 +19,7 @@ from django_cfg.modules.django_dashboard.sections.documentation import Documenta
|
|
|
19
19
|
from django_cfg.modules.django_dashboard.sections.overview import OverviewSection
|
|
20
20
|
from django_cfg.modules.django_dashboard.sections.stats import StatsSection
|
|
21
21
|
from django_cfg.modules.django_dashboard.sections.system import SystemSection
|
|
22
|
+
from django_cfg.modules.django_dashboard.sections.widgets import WidgetsSection
|
|
22
23
|
|
|
23
24
|
from ...base import BaseCfgModule
|
|
24
25
|
from ..models.dashboard import DashboardData
|
|
@@ -155,6 +156,29 @@ class UnfoldCallbacks(
|
|
|
155
156
|
logger.error(f"Failed to render documentation section: {e}", exc_info=True)
|
|
156
157
|
documentation_section = None
|
|
157
158
|
|
|
159
|
+
# Extract custom widgets from context if provided by project's dashboard_callback
|
|
160
|
+
custom_widgets = context.get('custom_widgets', [])
|
|
161
|
+
custom_metrics = {}
|
|
162
|
+
|
|
163
|
+
# Extract all metric-like variables from context for widget template resolution
|
|
164
|
+
# This allows dashboard_callback to add metrics like: context['total_users'] = 123
|
|
165
|
+
for key, value in context.items():
|
|
166
|
+
if key not in ['request', 'cards', 'system_health', 'quick_actions'] and isinstance(value, (int, float, str)):
|
|
167
|
+
custom_metrics[key] = value
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
# Render widgets section with custom widgets and metrics from callback
|
|
171
|
+
widgets_section = WidgetsSection(request).render(
|
|
172
|
+
custom_widgets=custom_widgets,
|
|
173
|
+
custom_metrics=custom_metrics
|
|
174
|
+
)
|
|
175
|
+
# Debug: save render (only in debug mode)
|
|
176
|
+
if config and config.debug:
|
|
177
|
+
save_section_render('widgets', widgets_section)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Failed to render widgets section: {e}", exc_info=True)
|
|
180
|
+
widgets_section = None
|
|
181
|
+
|
|
158
182
|
# Combine all stat cards (data already loaded above)
|
|
159
183
|
all_stats = user_stats + support_stats
|
|
160
184
|
|
|
@@ -176,6 +200,7 @@ class UnfoldCallbacks(
|
|
|
176
200
|
"system_section": system_section,
|
|
177
201
|
"commands_section": commands_section,
|
|
178
202
|
"documentation_section": documentation_section,
|
|
203
|
+
"widgets_section": widgets_section,
|
|
179
204
|
|
|
180
205
|
# Statistics cards
|
|
181
206
|
"cards": cards_data,
|
|
@@ -252,24 +277,24 @@ class UnfoldCallbacks(
|
|
|
252
277
|
})
|
|
253
278
|
|
|
254
279
|
# Log charts data for debugging
|
|
255
|
-
charts_data = context.get('charts', {})
|
|
256
|
-
logger.info(f"Charts data added to context: {list(charts_data.keys())}")
|
|
257
|
-
if 'user_registrations' in charts_data:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if 'user_activity' in charts_data:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
# Log recent users data for debugging
|
|
265
|
-
recent_users_data = context.get('recent_users', [])
|
|
266
|
-
logger.info(f"Recent users data count: {len(recent_users_data)}")
|
|
267
|
-
if recent_users_data:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# Log activity tracker data for debugging
|
|
271
|
-
activity_tracker_data = context.get('activity_tracker', [])
|
|
272
|
-
logger.info(f"Activity tracker data count: {len(activity_tracker_data)}")
|
|
280
|
+
# charts_data = context.get('charts', {})
|
|
281
|
+
# # logger.info(f"Charts data added to context: {list(charts_data.keys())}")
|
|
282
|
+
# if 'user_registrations' in charts_data:
|
|
283
|
+
# reg_data = charts_data['user_registrations']
|
|
284
|
+
# logger.info(f"Registration chart labels: {reg_data.get('labels', [])}")
|
|
285
|
+
# if 'user_activity' in charts_data:
|
|
286
|
+
# act_data = charts_data['user_activity']
|
|
287
|
+
# logger.info(f"Activity chart labels: {act_data.get('labels', [])}")
|
|
288
|
+
|
|
289
|
+
# # Log recent users data for debugging
|
|
290
|
+
# recent_users_data = context.get('recent_users', [])
|
|
291
|
+
# logger.info(f"Recent users data count: {len(recent_users_data)}")
|
|
292
|
+
# if recent_users_data:
|
|
293
|
+
# logger.info(f"First user: {recent_users_data[0].get('username', 'N/A')}")
|
|
294
|
+
|
|
295
|
+
# # Log activity tracker data for debugging
|
|
296
|
+
# activity_tracker_data = context.get('activity_tracker', [])
|
|
297
|
+
# logger.info(f"Activity tracker data count: {len(activity_tracker_data)}")
|
|
273
298
|
|
|
274
299
|
return context
|
|
275
300
|
|
|
@@ -73,9 +73,10 @@ class DashboardManager(BaseCfgModule):
|
|
|
73
73
|
|
|
74
74
|
# RPC Dashboard (if enabled)
|
|
75
75
|
if self.is_rpc_enabled():
|
|
76
|
-
operations_items.
|
|
77
|
-
NavigationItem(title="IPC/RPC Dashboard", icon=Icons.MONITOR_HEART, link="/cfg/ipc/admin/")
|
|
78
|
-
|
|
76
|
+
operations_items.extend([
|
|
77
|
+
NavigationItem(title="IPC/RPC Dashboard", icon=Icons.MONITOR_HEART, link="/cfg/ipc/admin/"),
|
|
78
|
+
NavigationItem(title="RPC Logs", icon=Icons.LIST_ALT, link=str(reverse_lazy("admin:django_cfg_ipc_rpclog_changelist"))),
|
|
79
|
+
])
|
|
79
80
|
|
|
80
81
|
# Background Tasks (if enabled)
|
|
81
82
|
if self.should_enable_tasks():
|
|
@@ -298,6 +299,8 @@ class DashboardManager(BaseCfgModule):
|
|
|
298
299
|
|
|
299
300
|
def get_widgets_config(self) -> List[Dict[str, Any]]:
|
|
300
301
|
"""Get dashboard widgets configuration using Pydantic models."""
|
|
302
|
+
widgets = []
|
|
303
|
+
|
|
301
304
|
# Create system overview widget with StatCard models
|
|
302
305
|
system_overview_widget = StatsCardsWidget(
|
|
303
306
|
title="System Overview",
|
|
@@ -322,9 +325,43 @@ class DashboardManager(BaseCfgModule):
|
|
|
322
325
|
),
|
|
323
326
|
]
|
|
324
327
|
)
|
|
328
|
+
widgets.append(system_overview_widget.to_dict())
|
|
329
|
+
|
|
330
|
+
# Add RPC monitoring widget if IPC is enabled
|
|
331
|
+
if self.is_rpc_enabled():
|
|
332
|
+
rpc_monitoring_widget = StatsCardsWidget(
|
|
333
|
+
title="IPC/RPC Monitoring",
|
|
334
|
+
cards=[
|
|
335
|
+
StatCard(
|
|
336
|
+
title="Total Calls",
|
|
337
|
+
value="{{ rpc_total_calls }}",
|
|
338
|
+
icon=Icons.API,
|
|
339
|
+
color="blue",
|
|
340
|
+
),
|
|
341
|
+
StatCard(
|
|
342
|
+
title="Success Rate",
|
|
343
|
+
value="{{ rpc_success_rate }}%",
|
|
344
|
+
icon=Icons.CHECK_CIRCLE,
|
|
345
|
+
color="green",
|
|
346
|
+
),
|
|
347
|
+
StatCard(
|
|
348
|
+
title="Avg Response Time",
|
|
349
|
+
value="{{ rpc_avg_duration }}ms",
|
|
350
|
+
icon=Icons.SPEED,
|
|
351
|
+
color="purple",
|
|
352
|
+
),
|
|
353
|
+
StatCard(
|
|
354
|
+
title="Failed Calls",
|
|
355
|
+
value="{{ rpc_failed_calls }}",
|
|
356
|
+
icon=Icons.ERROR,
|
|
357
|
+
color="red",
|
|
358
|
+
),
|
|
359
|
+
]
|
|
360
|
+
)
|
|
361
|
+
widgets.append(rpc_monitoring_widget.to_dict())
|
|
325
362
|
|
|
326
363
|
# Convert to dictionaries for Unfold
|
|
327
|
-
return
|
|
364
|
+
return widgets
|
|
328
365
|
|
|
329
366
|
|
|
330
367
|
# Lazy initialization to avoid circular imports
|
django_cfg/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-cfg"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.60"
|
|
8
8
|
description = "Django AI framework with built-in agents, type-safe Pydantic v2 configuration, and 8 enterprise apps. Replace settings.py, validate at startup, 90% less code. Production-ready AI workflows for Django."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "ai-agents", "enterprise-django", "django-settings", "type-safe-config",]
|