django-cfg 1.2.22__py3-none-any.whl → 1.2.23__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/config/__init__.py +87 -0
- django_cfg/apps/payments/config/module.py +162 -0
- django_cfg/apps/payments/config/providers.py +93 -0
- django_cfg/apps/payments/config/settings.py +136 -0
- django_cfg/apps/payments/config/utils.py +198 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/middleware/api_access.py +261 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +32 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +2 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +58 -7
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +449 -0
- django_cfg/apps/payments/services/core/payment_service.py +393 -0
- django_cfg/apps/payments/services/core/subscription_service.py +616 -0
- django_cfg/apps/payments/services/internal_types.py +266 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/providers/__init__.py +19 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +99 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +150 -0
- django_cfg/apps/payments/signals/payment_signals.py +127 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +42 -0
- django_cfg/apps/payments/utils/config_utils.py +243 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/models/revolution.py +1 -1
- django_cfg/modules/base.py +1 -1
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/RECORD +63 -26
- django_cfg/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
"""
|
2
|
+
Utilities for middleware processing.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Optional, List
|
6
|
+
from django.http import HttpRequest
|
7
|
+
from django.conf import settings
|
8
|
+
|
9
|
+
|
10
|
+
def get_client_ip(request: HttpRequest) -> Optional[str]:
|
11
|
+
"""
|
12
|
+
Get client IP address from request.
|
13
|
+
Handles various proxy headers and configurations.
|
14
|
+
"""
|
15
|
+
|
16
|
+
# Check for forwarded headers first
|
17
|
+
forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
18
|
+
if forwarded_for:
|
19
|
+
# Take first IP in chain (original client)
|
20
|
+
return forwarded_for.split(',')[0].strip()
|
21
|
+
|
22
|
+
# Check for real IP header (common with nginx)
|
23
|
+
real_ip = request.META.get('HTTP_X_REAL_IP')
|
24
|
+
if real_ip:
|
25
|
+
return real_ip.strip()
|
26
|
+
|
27
|
+
# Check for Cloudflare header
|
28
|
+
cf_ip = request.META.get('HTTP_CF_CONNECTING_IP')
|
29
|
+
if cf_ip:
|
30
|
+
return cf_ip.strip()
|
31
|
+
|
32
|
+
# Fallback to remote address
|
33
|
+
remote_addr = request.META.get('REMOTE_ADDR')
|
34
|
+
if remote_addr:
|
35
|
+
return remote_addr.strip()
|
36
|
+
|
37
|
+
return None
|
38
|
+
|
39
|
+
|
40
|
+
def is_api_request(request: HttpRequest, api_prefixes: Optional[List[str]] = None) -> bool:
|
41
|
+
"""
|
42
|
+
Check if request is an API request based on path prefixes.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
request: Django HTTP request
|
46
|
+
api_prefixes: List of API prefixes to check (defaults to settings)
|
47
|
+
"""
|
48
|
+
|
49
|
+
if api_prefixes is None:
|
50
|
+
api_prefixes = getattr(settings, 'PAYMENTS_API_PREFIXES', ['/api/'])
|
51
|
+
|
52
|
+
path = request.path
|
53
|
+
return any(path.startswith(prefix) for prefix in api_prefixes)
|
54
|
+
|
55
|
+
|
56
|
+
def extract_api_key(request: HttpRequest) -> Optional[str]:
|
57
|
+
"""
|
58
|
+
Extract API key from request headers or query parameters.
|
59
|
+
Supports multiple authentication methods.
|
60
|
+
|
61
|
+
Priority:
|
62
|
+
1. Authorization header (Bearer token)
|
63
|
+
2. X-API-Key header
|
64
|
+
3. Query parameter (less secure, for testing)
|
65
|
+
"""
|
66
|
+
|
67
|
+
# Method 1: Authorization header (Bearer token)
|
68
|
+
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
69
|
+
if auth_header.startswith('Bearer '):
|
70
|
+
return auth_header[7:] # Remove 'Bearer ' prefix
|
71
|
+
|
72
|
+
# Method 2: X-API-Key header
|
73
|
+
api_key_header = request.META.get('HTTP_X_API_KEY')
|
74
|
+
if api_key_header:
|
75
|
+
return api_key_header.strip()
|
76
|
+
|
77
|
+
# Method 3: Custom header variations
|
78
|
+
custom_headers = [
|
79
|
+
'HTTP_X_API_TOKEN',
|
80
|
+
'HTTP_APIKEY',
|
81
|
+
'HTTP_API_TOKEN',
|
82
|
+
]
|
83
|
+
|
84
|
+
for header in custom_headers:
|
85
|
+
value = request.META.get(header)
|
86
|
+
if value:
|
87
|
+
return value.strip()
|
88
|
+
|
89
|
+
# Method 4: Query parameter (less secure, mainly for testing)
|
90
|
+
if getattr(settings, 'PAYMENTS_ALLOW_API_KEY_IN_QUERY', False):
|
91
|
+
query_key = request.GET.get('api_key') or request.GET.get('apikey')
|
92
|
+
if query_key:
|
93
|
+
return query_key.strip()
|
94
|
+
|
95
|
+
return None
|
96
|
+
|
97
|
+
|
98
|
+
def is_exempt_path(request: HttpRequest, exempt_paths: Optional[List[str]] = None) -> bool:
|
99
|
+
"""
|
100
|
+
Check if request path is exempt from API key requirements.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
request: Django HTTP request
|
104
|
+
exempt_paths: List of exempt path prefixes (defaults to settings)
|
105
|
+
"""
|
106
|
+
|
107
|
+
if exempt_paths is None:
|
108
|
+
exempt_paths = getattr(settings, 'PAYMENTS_EXEMPT_PATHS', [
|
109
|
+
'/admin/',
|
110
|
+
'/cfg/',
|
111
|
+
'/api/v1/api-key/validate/',
|
112
|
+
])
|
113
|
+
|
114
|
+
path = request.path
|
115
|
+
return any(path.startswith(exempt) for exempt in exempt_paths)
|
116
|
+
|
117
|
+
|
118
|
+
def get_request_metadata(request: HttpRequest) -> dict:
|
119
|
+
"""
|
120
|
+
Extract useful metadata from request for logging and analytics.
|
121
|
+
"""
|
122
|
+
|
123
|
+
return {
|
124
|
+
'method': request.method,
|
125
|
+
'path': request.path,
|
126
|
+
'query_string': request.META.get('QUERY_STRING', ''),
|
127
|
+
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
128
|
+
'referer': request.META.get('HTTP_REFERER', ''),
|
129
|
+
'ip_address': get_client_ip(request),
|
130
|
+
'content_type': request.META.get('CONTENT_TYPE', ''),
|
131
|
+
'content_length': request.META.get('CONTENT_LENGTH', 0),
|
132
|
+
'host': request.META.get('HTTP_HOST', ''),
|
133
|
+
'scheme': request.scheme,
|
134
|
+
'is_secure': request.is_secure(),
|
135
|
+
}
|
136
|
+
|
137
|
+
|
138
|
+
def should_track_request_body(request: HttpRequest, max_size: int = 10000) -> bool:
|
139
|
+
"""
|
140
|
+
Determine if request body should be tracked for analytics.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
request: Django HTTP request
|
144
|
+
max_size: Maximum body size to track (bytes)
|
145
|
+
"""
|
146
|
+
|
147
|
+
# Check content length
|
148
|
+
content_length = request.META.get('CONTENT_LENGTH')
|
149
|
+
if content_length and int(content_length) > max_size:
|
150
|
+
return False
|
151
|
+
|
152
|
+
# Don't track file uploads
|
153
|
+
content_type = request.META.get('CONTENT_TYPE', '')
|
154
|
+
if 'multipart/form-data' in content_type:
|
155
|
+
return False
|
156
|
+
|
157
|
+
# Don't track binary content
|
158
|
+
if 'application/octet-stream' in content_type:
|
159
|
+
return False
|
160
|
+
|
161
|
+
# Don't track sensitive endpoints
|
162
|
+
sensitive_paths = getattr(settings, 'PAYMENTS_SENSITIVE_PATHS', [
|
163
|
+
'/api/v1/api-key/',
|
164
|
+
'/api/v1/payment/',
|
165
|
+
'/api/v1/subscription/',
|
166
|
+
])
|
167
|
+
|
168
|
+
path = request.path
|
169
|
+
if any(path.startswith(sensitive) for sensitive in sensitive_paths):
|
170
|
+
return False
|
171
|
+
|
172
|
+
return True
|
173
|
+
|
174
|
+
|
175
|
+
def should_track_response_body(response, max_size: int = 10000) -> bool:
|
176
|
+
"""
|
177
|
+
Determine if response body should be tracked for analytics.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
response: Django HTTP response
|
181
|
+
max_size: Maximum body size to track (bytes)
|
182
|
+
"""
|
183
|
+
|
184
|
+
# Don't track large responses
|
185
|
+
if hasattr(response, 'content') and len(response.content) > max_size:
|
186
|
+
return False
|
187
|
+
|
188
|
+
# Only track successful JSON responses
|
189
|
+
if not (200 <= response.status_code < 300):
|
190
|
+
return False
|
191
|
+
|
192
|
+
# Check content type
|
193
|
+
content_type = response.get('Content-Type', '')
|
194
|
+
if 'application/json' not in content_type:
|
195
|
+
return False
|
196
|
+
|
197
|
+
return True
|
198
|
+
|
199
|
+
|
200
|
+
def format_error_response(error_code: str,
|
201
|
+
message: str,
|
202
|
+
status_code: int = 400,
|
203
|
+
additional_data: Optional[dict] = None) -> dict:
|
204
|
+
"""
|
205
|
+
Format standardized error response for middleware.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
error_code: Machine-readable error code
|
209
|
+
message: Human-readable error message
|
210
|
+
status_code: HTTP status code
|
211
|
+
additional_data: Additional error data
|
212
|
+
"""
|
213
|
+
|
214
|
+
from django.utils import timezone
|
215
|
+
|
216
|
+
error_response = {
|
217
|
+
'error': {
|
218
|
+
'code': error_code,
|
219
|
+
'message': message,
|
220
|
+
'status_code': status_code,
|
221
|
+
'timestamp': timezone.now().isoformat(),
|
222
|
+
}
|
223
|
+
}
|
224
|
+
|
225
|
+
if additional_data:
|
226
|
+
error_response['error'].update(additional_data)
|
227
|
+
|
228
|
+
return error_response
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Validation utilities for payments module.
|
3
|
+
|
4
|
+
Basic validation functions for API keys and subscription access.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Optional, Dict, Any
|
9
|
+
from django.contrib.auth import get_user_model
|
10
|
+
from django.utils import timezone
|
11
|
+
|
12
|
+
from ..models import APIKey, Subscription
|
13
|
+
|
14
|
+
User = get_user_model()
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
def validate_api_key(api_key: str) -> bool:
|
19
|
+
"""
|
20
|
+
Validate API key.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
api_key: API key to validate
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
True if valid, False otherwise
|
27
|
+
"""
|
28
|
+
try:
|
29
|
+
key = APIKey.objects.select_related('user').get(
|
30
|
+
key_value=api_key,
|
31
|
+
is_active=True
|
32
|
+
)
|
33
|
+
|
34
|
+
# Check if key is expired
|
35
|
+
if key.expires_at and key.expires_at < timezone.now():
|
36
|
+
return False
|
37
|
+
|
38
|
+
return True
|
39
|
+
|
40
|
+
except APIKey.DoesNotExist:
|
41
|
+
return False
|
42
|
+
except Exception as e:
|
43
|
+
logger.error(f"Error validating API key: {e}")
|
44
|
+
return False
|
45
|
+
|
46
|
+
|
47
|
+
def check_subscription_access(user_id: int, endpoint_group: str) -> Dict[str, Any]:
|
48
|
+
"""
|
49
|
+
Check subscription access for user and endpoint group.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
user_id: User ID
|
53
|
+
endpoint_group: Endpoint group name
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Access check result dictionary
|
57
|
+
"""
|
58
|
+
try:
|
59
|
+
subscription = Subscription.objects.select_related('endpoint_group').get(
|
60
|
+
user_id=user_id,
|
61
|
+
endpoint_group__name=endpoint_group,
|
62
|
+
status='active',
|
63
|
+
expires_at__gt=timezone.now()
|
64
|
+
)
|
65
|
+
|
66
|
+
# Check usage limits
|
67
|
+
usage_percentage = (subscription.current_usage / subscription.monthly_limit) * 100
|
68
|
+
remaining_requests = subscription.monthly_limit - subscription.current_usage
|
69
|
+
|
70
|
+
return {
|
71
|
+
'allowed': remaining_requests > 0,
|
72
|
+
'subscription_id': str(subscription.id),
|
73
|
+
'remaining_requests': remaining_requests,
|
74
|
+
'usage_percentage': usage_percentage,
|
75
|
+
'reason': 'Active subscription' if remaining_requests > 0 else 'Usage limit exceeded'
|
76
|
+
}
|
77
|
+
|
78
|
+
except Subscription.DoesNotExist:
|
79
|
+
return {
|
80
|
+
'allowed': False,
|
81
|
+
'reason': 'No active subscription found',
|
82
|
+
'subscription_id': None,
|
83
|
+
'remaining_requests': 0,
|
84
|
+
'usage_percentage': 0
|
85
|
+
}
|
86
|
+
except Exception as e:
|
87
|
+
logger.error(f"Error checking subscription access: {e}")
|
88
|
+
return {
|
89
|
+
'allowed': False,
|
90
|
+
'reason': f'Access check failed: {str(e)}',
|
91
|
+
'subscription_id': None,
|
92
|
+
'remaining_requests': 0,
|
93
|
+
'usage_percentage': 0
|
94
|
+
}
|
@@ -2,6 +2,8 @@ from django.db.models.signals import post_save
|
|
2
2
|
from django.dispatch import receiver
|
3
3
|
import traceback
|
4
4
|
import logging
|
5
|
+
import socket
|
6
|
+
from smtplib import SMTPException
|
5
7
|
|
6
8
|
from .models import Message, Ticket
|
7
9
|
from .utils.support_email_service import SupportEmailService
|
@@ -10,7 +12,7 @@ from django_cfg.modules.django_telegram import DjangoTelegram
|
|
10
12
|
logger = logging.getLogger(__name__)
|
11
13
|
|
12
14
|
@receiver(post_save, sender=Message)
|
13
|
-
def notify_on_message(sender, instance, created, **kwargs):
|
15
|
+
def notify_on_message(sender, instance: Message, created: bool, **kwargs):
|
14
16
|
"""Send notifications when a new message is created."""
|
15
17
|
logger.info(f"🔔 Signal triggered: Message {instance.uuid} created={created}")
|
16
18
|
|
@@ -32,9 +34,14 @@ def notify_on_message(sender, instance, created, **kwargs):
|
|
32
34
|
email_service = SupportEmailService(user)
|
33
35
|
email_service.send_support_reply_email(instance)
|
34
36
|
logger.info(f" 📬 Email sent successfully!")
|
37
|
+
except (socket.timeout, TimeoutError, SMTPException) as e:
|
38
|
+
logger.warning(f" ⚠️ Email service timeout/error: {e}")
|
39
|
+
logger.info(f" 📝 Message processed successfully, email notification failed")
|
40
|
+
# Do not re-raise to prevent blocking the main process
|
35
41
|
except Exception as e:
|
36
42
|
logger.error(f" ❌ Failed to send email notification: {e}")
|
37
|
-
traceback.
|
43
|
+
logger.debug(f" 🔍 Exception details: {traceback.format_exc()}")
|
44
|
+
# Do not re-raise to prevent blocking the main process
|
38
45
|
else:
|
39
46
|
logger.info(f" ⏭️ Not sending email (staff: {instance.sender.is_staff}, from_author: {instance.is_from_author})")
|
40
47
|
|
@@ -60,7 +67,7 @@ def notify_on_message(sender, instance, created, **kwargs):
|
|
60
67
|
|
61
68
|
|
62
69
|
@receiver(post_save, sender=Ticket)
|
63
|
-
def notify_on_ticket_created(sender, instance, created, **kwargs):
|
70
|
+
def notify_on_ticket_created(sender, instance: Ticket, created: bool, **kwargs):
|
64
71
|
"""Send notification when a new ticket is created."""
|
65
72
|
if not created:
|
66
73
|
return
|
@@ -68,5 +75,10 @@ def notify_on_ticket_created(sender, instance, created, **kwargs):
|
|
68
75
|
try:
|
69
76
|
email_service = SupportEmailService(instance.user)
|
70
77
|
email_service.send_ticket_created_email(instance)
|
78
|
+
logger.info(f" 📬 Ticket creation email sent successfully!")
|
79
|
+
except (socket.timeout, TimeoutError, SMTPException) as e:
|
80
|
+
logger.warning(f" ⚠️ Email service timeout/error for ticket creation: {e}")
|
81
|
+
logger.info(f" 📝 Ticket created successfully, email notification failed")
|
71
82
|
except Exception as e:
|
72
|
-
logger.error(f"Failed to send ticket creation email: {e}")
|
83
|
+
logger.error(f" ❌ Failed to send ticket creation email: {e}")
|
84
|
+
logger.debug(f" 🔍 Exception details: {traceback.format_exc()}")
|
@@ -197,7 +197,7 @@
|
|
197
197
|
submitBtn.disabled = true;
|
198
198
|
|
199
199
|
try {
|
200
|
-
const response = await fetch(`{% url 'send-message-ajax' ticket_uuid=ticket.uuid %}`, {
|
200
|
+
const response = await fetch(`{% url 'cfg_support:send-message-ajax' ticket_uuid=ticket.uuid %}`, {
|
201
201
|
method: 'POST',
|
202
202
|
headers: {
|
203
203
|
'Content-Type': 'application/json',
|
django_cfg/models/revolution.py
CHANGED
@@ -93,7 +93,7 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
|
|
93
93
|
knowbase_enabled = base_module.is_knowbase_enabled()
|
94
94
|
agents_enabled = base_module.is_agents_enabled()
|
95
95
|
tasks_enabled = base_module.should_enable_tasks()
|
96
|
-
payments_enabled = base_module.
|
96
|
+
payments_enabled = base_module.is_payments_enabled()
|
97
97
|
|
98
98
|
# Add Support zone if enabled
|
99
99
|
default_support_zone = 'cfg_support'
|
django_cfg/modules/base.py
CHANGED
@@ -6,6 +6,9 @@ without requiring manual parameter passing.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
from typing import List, Optional, Dict, Any, Union
|
9
|
+
import socket
|
10
|
+
from smtplib import SMTPException
|
11
|
+
import logging
|
9
12
|
from django.core.mail import send_mail, EmailMultiAlternatives
|
10
13
|
from django.template.loader import render_to_string
|
11
14
|
from django.utils.html import strip_tags
|
@@ -13,6 +16,8 @@ from django.conf import settings
|
|
13
16
|
|
14
17
|
from . import BaseCfgModule
|
15
18
|
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
16
21
|
|
17
22
|
class DjangoEmailService(BaseCfgModule):
|
18
23
|
"""
|
@@ -30,6 +35,35 @@ class DjangoEmailService(BaseCfgModule):
|
|
30
35
|
self.config = self.get_config()
|
31
36
|
self.email_config = getattr(self.config, 'email', None)
|
32
37
|
|
38
|
+
def _handle_email_sending(self, email_func, *args, **kwargs):
|
39
|
+
"""
|
40
|
+
Wrapper for email sending with proper timeout/exception handling.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
email_func: Email sending function to call
|
44
|
+
*args, **kwargs: Arguments to pass to the function
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Result of email_func or 0 on timeout/error
|
48
|
+
"""
|
49
|
+
try:
|
50
|
+
result = email_func(*args, **kwargs)
|
51
|
+
logger.debug(f"Email sent successfully: {email_func.__name__}")
|
52
|
+
return result
|
53
|
+
except (socket.timeout, TimeoutError) as e:
|
54
|
+
logger.warning(f"Email sending timeout: {e}")
|
55
|
+
logger.info("Consider checking SMTP server configuration or network connectivity")
|
56
|
+
return 0
|
57
|
+
except SMTPException as e:
|
58
|
+
logger.warning(f"SMTP error during email sending: {e}")
|
59
|
+
logger.info("Email service temporarily unavailable")
|
60
|
+
return 0
|
61
|
+
except Exception as e:
|
62
|
+
logger.error(f"Unexpected error during email sending: {e}")
|
63
|
+
if not kwargs.get('fail_silently', False):
|
64
|
+
raise
|
65
|
+
return 0
|
66
|
+
|
33
67
|
def send_simple(
|
34
68
|
self,
|
35
69
|
subject: str,
|
@@ -53,7 +87,8 @@ class DjangoEmailService(BaseCfgModule):
|
|
53
87
|
"""
|
54
88
|
from_email = self._get_formatted_from_email(from_email)
|
55
89
|
|
56
|
-
return
|
90
|
+
return self._handle_email_sending(
|
91
|
+
send_mail,
|
57
92
|
subject=subject,
|
58
93
|
message=message,
|
59
94
|
from_email=from_email,
|
@@ -89,7 +124,8 @@ class DjangoEmailService(BaseCfgModule):
|
|
89
124
|
if text_message is None:
|
90
125
|
text_message = strip_tags(html_message)
|
91
126
|
|
92
|
-
return
|
127
|
+
return self._handle_email_sending(
|
128
|
+
send_mail,
|
93
129
|
subject=subject,
|
94
130
|
message=text_message,
|
95
131
|
from_email=from_email,
|
@@ -174,7 +210,7 @@ class DjangoEmailService(BaseCfgModule):
|
|
174
210
|
if not html_content and not text_content:
|
175
211
|
raise ValueError("Either html_content or text_content must be provided")
|
176
212
|
|
177
|
-
|
213
|
+
def _send_multipart_email():
|
178
214
|
email = EmailMultiAlternatives(
|
179
215
|
subject=subject,
|
180
216
|
body=text_content or strip_tags(html_content or ''),
|
@@ -191,7 +227,9 @@ class DjangoEmailService(BaseCfgModule):
|
|
191
227
|
|
192
228
|
email.send(fail_silently=fail_silently)
|
193
229
|
return True
|
194
|
-
|
230
|
+
|
231
|
+
try:
|
232
|
+
return self._handle_email_sending(_send_multipart_email) or False
|
195
233
|
except Exception as e:
|
196
234
|
if not fail_silently:
|
197
235
|
raise e
|
@@ -163,8 +163,28 @@ class DashboardManager(BaseCfgModule):
|
|
163
163
|
]
|
164
164
|
))
|
165
165
|
|
166
|
+
# Add Payments section if enabled
|
167
|
+
if self.is_payments_enabled():
|
168
|
+
navigation_sections.append(NavigationSection(
|
169
|
+
title="Payments",
|
170
|
+
separator=True,
|
171
|
+
collapsible=True,
|
172
|
+
items=[
|
173
|
+
NavigationItem(title="Payments", icon=Icons.ACCOUNT_BALANCE, link="/admin/django_cfg_payments/universalpayment/"),
|
174
|
+
NavigationItem(title="Subscriptions", icon=Icons.PERSON_ADD, link="/admin/django_cfg_payments/subscription/"),
|
175
|
+
NavigationItem(title="API Keys", icon=Icons.KEY, link="/admin/django_cfg_payments/apikey/"),
|
176
|
+
NavigationItem(title="Balances", icon=Icons.ACCOUNT_BALANCE_WALLET, link="/admin/django_cfg_payments/userbalance/"),
|
177
|
+
NavigationItem(title="Transactions", icon=Icons.DESCRIPTION, link="/admin/django_cfg_payments/transaction/"),
|
178
|
+
NavigationItem(title="Currencies", icon=Icons.ACCOUNT_CIRCLE, link="/admin/django_cfg_payments/currency/"),
|
179
|
+
NavigationItem(title="Currency Networks", icon=Icons.LINK, link="/admin/django_cfg_payments/currencynetwork/"),
|
180
|
+
NavigationItem(title="Endpoint Groups", icon=Icons.GROUP, link="/admin/django_cfg_payments/endpointgroup/"),
|
181
|
+
NavigationItem(title="Tariffs", icon=Icons.SETTINGS, link="/admin/django_cfg_payments/tariff/"),
|
182
|
+
]
|
183
|
+
))
|
184
|
+
|
166
185
|
# Convert all NavigationSection objects to dictionaries
|
167
186
|
return [section.to_dict() for section in navigation_sections]
|
187
|
+
|
168
188
|
|
169
189
|
|
170
190
|
def get_unfold_config(self) -> Dict[str, Any]:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-cfg
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.23
|
4
4
|
Summary: 🚀 Next-gen Django configuration: type-safety, AI features, blazing-fast setup, and automated best practices — all in one.
|
5
5
|
Project-URL: Homepage, https://djangocfg.com
|
6
6
|
Project-URL: Documentation, https://docs.djangocfg.com
|
@@ -146,6 +146,7 @@ Description-Content-Type: text/markdown
|
|
146
146
|
|
147
147
|
# 🚀 Django-CFG: Enterprise Django Configuration Framework
|
148
148
|
|
149
|
+
|
149
150
|
[](https://pypi.org/project/django-cfg/)
|
150
151
|
[](https://pypi.org/project/django-cfg/)
|
151
152
|
[](https://pypi.org/project/django-cfg/)
|