django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- 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 +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +294 -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 +125 -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 +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -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 +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -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 +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- 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/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
@@ -23,7 +23,7 @@ User = get_user_model()
|
|
23
23
|
|
24
24
|
|
25
25
|
@dramatiq.actor(
|
26
|
-
queue_name="
|
26
|
+
queue_name="knowbase",
|
27
27
|
max_retries=3,
|
28
28
|
min_backoff=1000, # 1 second
|
29
29
|
max_backoff=30000, # 30 seconds
|
@@ -96,7 +96,7 @@ def process_archive_task(archive_id: str, user_id: str) -> bool:
|
|
96
96
|
|
97
97
|
|
98
98
|
@dramatiq.actor(
|
99
|
-
queue_name="
|
99
|
+
queue_name="knowbase",
|
100
100
|
max_retries=2,
|
101
101
|
min_backoff=2000, # 2 seconds
|
102
102
|
max_backoff=60000, # 60 seconds
|
@@ -154,7 +154,7 @@ def vectorize_archive_items_task(archive_id: str, user_id: str) -> int:
|
|
154
154
|
|
155
155
|
|
156
156
|
@dramatiq.actor(
|
157
|
-
queue_name="
|
157
|
+
queue_name="knowbase",
|
158
158
|
max_retries=1,
|
159
159
|
priority=2
|
160
160
|
)
|
@@ -193,7 +193,7 @@ def cleanup_failed_archives_task(days_old: int = 7) -> int:
|
|
193
193
|
|
194
194
|
|
195
195
|
@dramatiq.actor(
|
196
|
-
queue_name="
|
196
|
+
queue_name="knowbase",
|
197
197
|
max_retries=1,
|
198
198
|
priority=1
|
199
199
|
)
|
@@ -239,7 +239,7 @@ def generate_archive_statistics_task(user_id: str) -> Dict[str, Any]:
|
|
239
239
|
|
240
240
|
|
241
241
|
@dramatiq.actor(
|
242
|
-
queue_name="
|
242
|
+
queue_name="knowbase",
|
243
243
|
max_retries=1,
|
244
244
|
priority=1
|
245
245
|
)
|
@@ -297,7 +297,7 @@ def archive_health_check_task() -> Dict[str, Any]:
|
|
297
297
|
|
298
298
|
# Test task for development
|
299
299
|
@dramatiq.actor(
|
300
|
-
queue_name="
|
300
|
+
queue_name="knowbase",
|
301
301
|
max_retries=0,
|
302
302
|
priority=1
|
303
303
|
)
|
@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
|
20
20
|
|
21
21
|
|
22
22
|
@dramatiq.actor(
|
23
|
-
queue_name="
|
23
|
+
queue_name="knowbase",
|
24
24
|
max_retries=3,
|
25
25
|
min_backoff=1000, # 1 second
|
26
26
|
max_backoff=30000, # 30 seconds
|
@@ -239,7 +239,7 @@ def generate_embeddings_batch(
|
|
239
239
|
|
240
240
|
|
241
241
|
@dramatiq.actor(
|
242
|
-
queue_name="
|
242
|
+
queue_name="knowbase",
|
243
243
|
max_retries=2,
|
244
244
|
priority=7 # Higher priority for reprocessing
|
245
245
|
)
|
@@ -291,7 +291,7 @@ def reprocess_document_chunks(
|
|
291
291
|
|
292
292
|
|
293
293
|
@dramatiq.actor(
|
294
|
-
queue_name="
|
294
|
+
queue_name="knowbase",
|
295
295
|
max_retries=2,
|
296
296
|
priority=4
|
297
297
|
)
|
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|
16
16
|
|
17
17
|
|
18
18
|
@dramatiq.actor(
|
19
|
-
queue_name="
|
19
|
+
queue_name="knowbase",
|
20
20
|
max_retries=3,
|
21
21
|
min_backoff=1000, # 1 second
|
22
22
|
max_backoff=30000, # 30 seconds
|
@@ -158,7 +158,7 @@ def process_external_data_async(
|
|
158
158
|
|
159
159
|
|
160
160
|
@dramatiq.actor(
|
161
|
-
queue_name="
|
161
|
+
queue_name="knowbase",
|
162
162
|
max_retries=2,
|
163
163
|
min_backoff=5000, # 5 seconds
|
164
164
|
max_backoff=60000, # 60 seconds
|
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
|
|
18
18
|
|
19
19
|
|
20
20
|
@dramatiq.actor(
|
21
|
-
queue_name="
|
21
|
+
queue_name="knowbase",
|
22
22
|
max_retries=1,
|
23
23
|
priority=2
|
24
24
|
)
|
@@ -72,7 +72,7 @@ def cleanup_old_embeddings(days_old: int = 90) -> Dict[str, Any]:
|
|
72
72
|
|
73
73
|
|
74
74
|
@dramatiq.actor(
|
75
|
-
queue_name="
|
75
|
+
queue_name="knowbase",
|
76
76
|
max_retries=1,
|
77
77
|
priority=1
|
78
78
|
)
|
@@ -119,7 +119,7 @@ def optimize_vector_indexes() -> Dict[str, Any]:
|
|
119
119
|
|
120
120
|
|
121
121
|
@dramatiq.actor(
|
122
|
-
queue_name="
|
122
|
+
queue_name="knowbase",
|
123
123
|
max_retries=1,
|
124
124
|
priority=1
|
125
125
|
)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"""
|
2
|
+
Admin interfaces for universal payments.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .balance_admin import UserBalanceAdmin, TransactionAdmin
|
6
|
+
from .payments_admin import UniversalPaymentAdmin
|
7
|
+
from .subscriptions_admin import SubscriptionAdmin, EndpointGroupAdmin
|
8
|
+
from .api_keys_admin import APIKeyAdmin
|
9
|
+
from .currencies_admin import CurrencyAdmin, CurrencyNetworkAdmin
|
10
|
+
from .tariffs_admin import TariffAdmin, TariffEndpointGroupAdmin
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
'UserBalanceAdmin',
|
14
|
+
'TransactionAdmin',
|
15
|
+
'UniversalPaymentAdmin',
|
16
|
+
'SubscriptionAdmin',
|
17
|
+
'EndpointGroupAdmin',
|
18
|
+
'APIKeyAdmin',
|
19
|
+
'CurrencyAdmin',
|
20
|
+
'CurrencyNetworkAdmin',
|
21
|
+
'TariffAdmin',
|
22
|
+
'TariffEndpointGroupAdmin',
|
23
|
+
]
|
@@ -0,0 +1,347 @@
|
|
1
|
+
"""
|
2
|
+
Admin interface for API key management.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.contrib import admin
|
6
|
+
from django.utils.html import format_html
|
7
|
+
from django.contrib.humanize.templatetags.humanize import naturaltime
|
8
|
+
from django.urls import reverse
|
9
|
+
from django.shortcuts import redirect
|
10
|
+
from unfold.admin import ModelAdmin
|
11
|
+
from unfold.decorators import display, action
|
12
|
+
from unfold.enums import ActionVariant
|
13
|
+
|
14
|
+
from ..models import APIKey
|
15
|
+
from .filters import APIKeyStatusFilter, UserEmailFilter, RecentActivityFilter
|
16
|
+
|
17
|
+
|
18
|
+
@admin.register(APIKey)
|
19
|
+
class APIKeyAdmin(ModelAdmin):
|
20
|
+
"""Admin interface for API keys."""
|
21
|
+
|
22
|
+
list_display = [
|
23
|
+
'key_display',
|
24
|
+
'user_display',
|
25
|
+
'name',
|
26
|
+
'status_display',
|
27
|
+
'usage_display',
|
28
|
+
'last_used_display',
|
29
|
+
'expires_display',
|
30
|
+
'created_at_display'
|
31
|
+
]
|
32
|
+
|
33
|
+
list_display_links = ['key_display', 'name']
|
34
|
+
|
35
|
+
search_fields = [
|
36
|
+
'name',
|
37
|
+
'user__email',
|
38
|
+
'user__first_name',
|
39
|
+
'user__last_name',
|
40
|
+
'key_value',
|
41
|
+
'key_prefix'
|
42
|
+
]
|
43
|
+
|
44
|
+
list_filter = [
|
45
|
+
APIKeyStatusFilter,
|
46
|
+
UserEmailFilter,
|
47
|
+
RecentActivityFilter,
|
48
|
+
'is_active',
|
49
|
+
'created_at',
|
50
|
+
'last_used'
|
51
|
+
]
|
52
|
+
|
53
|
+
readonly_fields = [
|
54
|
+
'key_value',
|
55
|
+
'key_prefix',
|
56
|
+
'usage_count',
|
57
|
+
'last_used',
|
58
|
+
'created_at',
|
59
|
+
'key_statistics',
|
60
|
+
'usage_history'
|
61
|
+
]
|
62
|
+
|
63
|
+
fieldsets = [
|
64
|
+
('API Key Information', {
|
65
|
+
'fields': ['name', 'user']
|
66
|
+
}),
|
67
|
+
('Key Details', {
|
68
|
+
'fields': ['key_value', 'key_prefix'],
|
69
|
+
'classes': ['collapse']
|
70
|
+
}),
|
71
|
+
('Settings', {
|
72
|
+
'fields': ['is_active', 'expires_at']
|
73
|
+
}),
|
74
|
+
('Usage Statistics', {
|
75
|
+
'fields': ['usage_count', 'last_used', 'key_statistics'],
|
76
|
+
'classes': ['collapse']
|
77
|
+
}),
|
78
|
+
('Usage History', {
|
79
|
+
'fields': ['usage_history'],
|
80
|
+
'classes': ['collapse']
|
81
|
+
}),
|
82
|
+
('Timestamps', {
|
83
|
+
'fields': ['created_at'],
|
84
|
+
'classes': ['collapse']
|
85
|
+
})
|
86
|
+
]
|
87
|
+
|
88
|
+
actions = [
|
89
|
+
'activate_keys',
|
90
|
+
'deactivate_keys',
|
91
|
+
'reset_usage'
|
92
|
+
]
|
93
|
+
|
94
|
+
actions_detail = [
|
95
|
+
'regenerate_key',
|
96
|
+
'view_usage_stats',
|
97
|
+
'deactivate_key'
|
98
|
+
]
|
99
|
+
|
100
|
+
@display(description="API Key")
|
101
|
+
def key_display(self, obj):
|
102
|
+
"""Display API key with masking."""
|
103
|
+
if obj.key_value:
|
104
|
+
masked_key = f"{obj.key_prefix}***{obj.key_value[-4:]}"
|
105
|
+
else:
|
106
|
+
masked_key = f"{obj.key_prefix}***"
|
107
|
+
|
108
|
+
status_color = '#28a745' if obj.is_active else '#dc3545'
|
109
|
+
status_icon = '🔑' if obj.is_active else '🔒'
|
110
|
+
|
111
|
+
return format_html(
|
112
|
+
'<span style="color: {};">{}</span> <code>{}</code>',
|
113
|
+
status_color,
|
114
|
+
status_icon,
|
115
|
+
masked_key
|
116
|
+
)
|
117
|
+
|
118
|
+
@display(description="User")
|
119
|
+
def user_display(self, obj):
|
120
|
+
"""Display user information."""
|
121
|
+
user = obj.user
|
122
|
+
if hasattr(user, 'avatar') and user.avatar:
|
123
|
+
avatar_html = f'<img src="{user.avatar.url}" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 6px;" />'
|
124
|
+
else:
|
125
|
+
initials = f"{user.first_name[:1]}{user.last_name[:1]}" if user.first_name and user.last_name else user.email[:2]
|
126
|
+
avatar_html = f'<div style="width: 20px; height: 20px; border-radius: 50%; background: #6c757d; color: white; display: inline-flex; align-items: center; justify-content: center; font-size: 9px; margin-right: 6px;">{initials.upper()}</div>'
|
127
|
+
|
128
|
+
return format_html(
|
129
|
+
'{}<strong>{}</strong><br><small>{}</small>',
|
130
|
+
avatar_html,
|
131
|
+
user.get_full_name() or user.email,
|
132
|
+
user.email
|
133
|
+
)
|
134
|
+
|
135
|
+
@display(description="Status")
|
136
|
+
def status_display(self, obj):
|
137
|
+
"""Display status with validation check."""
|
138
|
+
if not obj.is_active:
|
139
|
+
return format_html(
|
140
|
+
'<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Inactive</span>'
|
141
|
+
)
|
142
|
+
|
143
|
+
if obj.expires_at and obj.expires_at <= obj.__class__.objects.model._get_current_time():
|
144
|
+
return format_html(
|
145
|
+
'<span style="background: #ffc107; color: black; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Expired</span>'
|
146
|
+
)
|
147
|
+
|
148
|
+
if obj.is_valid():
|
149
|
+
return format_html(
|
150
|
+
'<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Active</span>'
|
151
|
+
)
|
152
|
+
else:
|
153
|
+
return format_html(
|
154
|
+
'<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Invalid</span>'
|
155
|
+
)
|
156
|
+
|
157
|
+
@display(description="Usage")
|
158
|
+
def usage_display(self, obj):
|
159
|
+
"""Display usage statistics."""
|
160
|
+
usage_count = obj.usage_count
|
161
|
+
|
162
|
+
if usage_count == 0:
|
163
|
+
color = '#6c757d'
|
164
|
+
text = 'Never used'
|
165
|
+
elif usage_count < 100:
|
166
|
+
color = '#28a745'
|
167
|
+
text = f'{usage_count} calls'
|
168
|
+
elif usage_count < 1000:
|
169
|
+
color = '#ffc107'
|
170
|
+
text = f'{usage_count} calls'
|
171
|
+
else:
|
172
|
+
color = '#dc3545'
|
173
|
+
text = f'{usage_count:,} calls'
|
174
|
+
|
175
|
+
return format_html(
|
176
|
+
'<span style="color: {}; font-weight: bold;">{}</span>',
|
177
|
+
color, text
|
178
|
+
)
|
179
|
+
|
180
|
+
@display(description="Last Used")
|
181
|
+
def last_used_display(self, obj):
|
182
|
+
"""Display last used time."""
|
183
|
+
if obj.last_used:
|
184
|
+
return naturaltime(obj.last_used)
|
185
|
+
return format_html(
|
186
|
+
'<span style="color: #6c757d;">Never</span>'
|
187
|
+
)
|
188
|
+
|
189
|
+
@display(description="Expires")
|
190
|
+
def expires_display(self, obj):
|
191
|
+
"""Display expiration time."""
|
192
|
+
if obj.expires_at:
|
193
|
+
from django.utils import timezone
|
194
|
+
if obj.expires_at <= timezone.now():
|
195
|
+
return format_html(
|
196
|
+
'<span style="color: #dc3545;">Expired</span>'
|
197
|
+
)
|
198
|
+
else:
|
199
|
+
return naturaltime(obj.expires_at)
|
200
|
+
return format_html(
|
201
|
+
'<span style="color: #6c757d;">Never</span>'
|
202
|
+
)
|
203
|
+
|
204
|
+
@display(description="Created")
|
205
|
+
def created_at_display(self, obj):
|
206
|
+
"""Display creation date."""
|
207
|
+
return naturaltime(obj.created_at)
|
208
|
+
|
209
|
+
def key_statistics(self, obj):
|
210
|
+
"""Show API key statistics."""
|
211
|
+
from django.utils import timezone
|
212
|
+
|
213
|
+
# Calculate usage trends
|
214
|
+
recent_usage = 0 # In real implementation, calculate from usage logs
|
215
|
+
|
216
|
+
return format_html(
|
217
|
+
'<div style="line-height: 1.6;">'
|
218
|
+
'<strong>API Key Statistics:</strong><br>'
|
219
|
+
'• Total Usage: <strong>{:,}</strong> calls<br>'
|
220
|
+
'• Status: {}<br>'
|
221
|
+
'• Valid: {}<br>'
|
222
|
+
'• Last Used: {}<br>'
|
223
|
+
'• Expires: {}<br>'
|
224
|
+
'• Created: {}<br>'
|
225
|
+
'</div>',
|
226
|
+
obj.usage_count,
|
227
|
+
'Active' if obj.is_active else 'Inactive',
|
228
|
+
'Yes' if obj.is_valid() else 'No',
|
229
|
+
naturaltime(obj.last_used) if obj.last_used else 'Never',
|
230
|
+
naturaltime(obj.expires_at) if obj.expires_at else 'Never',
|
231
|
+
naturaltime(obj.created_at)
|
232
|
+
)
|
233
|
+
|
234
|
+
key_statistics.short_description = "Key Statistics"
|
235
|
+
|
236
|
+
def usage_history(self, obj):
|
237
|
+
"""Show usage history (placeholder for future implementation)."""
|
238
|
+
return format_html(
|
239
|
+
'<div style="line-height: 1.6;">'
|
240
|
+
'<strong>Usage History:</strong><br>'
|
241
|
+
'• Total API Calls: {:,}<br>'
|
242
|
+
'• Last 24h: N/A<br>'
|
243
|
+
'• Last 7 days: N/A<br>'
|
244
|
+
'• Last 30 days: N/A<br>'
|
245
|
+
'<br>'
|
246
|
+
'<em>Detailed usage tracking will be implemented with analytics service.</em>'
|
247
|
+
'</div>',
|
248
|
+
obj.usage_count
|
249
|
+
)
|
250
|
+
|
251
|
+
usage_history.short_description = "Usage History"
|
252
|
+
|
253
|
+
# Admin Actions
|
254
|
+
|
255
|
+
@action(description="🔑 Regenerate API Key")
|
256
|
+
def regenerate_key(self, request, object_id):
|
257
|
+
"""Regenerate API key."""
|
258
|
+
api_key = self.get_object(request, object_id)
|
259
|
+
if not api_key:
|
260
|
+
self.message_user(request, "API key not found.", level='error')
|
261
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
262
|
+
|
263
|
+
# Generate new key
|
264
|
+
import secrets
|
265
|
+
api_key.key_value = f"ak_{secrets.token_urlsafe(32)}"
|
266
|
+
api_key.usage_count = 0 # Reset usage
|
267
|
+
api_key.save()
|
268
|
+
|
269
|
+
self.message_user(
|
270
|
+
request,
|
271
|
+
f"API key '{api_key.name}' has been regenerated. Usage count reset to 0.",
|
272
|
+
level='success'
|
273
|
+
)
|
274
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
275
|
+
|
276
|
+
@action(
|
277
|
+
description="📊 View Usage Statistics",
|
278
|
+
icon="analytics",
|
279
|
+
variant=ActionVariant.INFO
|
280
|
+
)
|
281
|
+
def view_usage_stats(self, request, object_id):
|
282
|
+
"""View detailed usage statistics."""
|
283
|
+
api_key = self.get_object(request, object_id)
|
284
|
+
if api_key:
|
285
|
+
self.message_user(
|
286
|
+
request,
|
287
|
+
f"Usage statistics view for '{api_key.name}' would open here.",
|
288
|
+
level='info'
|
289
|
+
)
|
290
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
291
|
+
|
292
|
+
@action(
|
293
|
+
description="🔒 Deactivate Key",
|
294
|
+
icon="block",
|
295
|
+
variant=ActionVariant.WARNING
|
296
|
+
)
|
297
|
+
def deactivate_key(self, request, object_id):
|
298
|
+
"""Deactivate API key."""
|
299
|
+
api_key = self.get_object(request, object_id)
|
300
|
+
if not api_key:
|
301
|
+
self.message_user(request, "API key not found.", level='error')
|
302
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
303
|
+
|
304
|
+
api_key.is_active = False
|
305
|
+
api_key.save()
|
306
|
+
|
307
|
+
self.message_user(
|
308
|
+
request,
|
309
|
+
f"API key '{api_key.name}' has been deactivated.",
|
310
|
+
level='warning'
|
311
|
+
)
|
312
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
313
|
+
|
314
|
+
# Bulk Actions
|
315
|
+
|
316
|
+
def activate_keys(self, request, queryset):
|
317
|
+
"""Activate selected API keys."""
|
318
|
+
count = queryset.update(is_active=True)
|
319
|
+
self.message_user(
|
320
|
+
request,
|
321
|
+
f"Successfully activated {count} API keys.",
|
322
|
+
level='success'
|
323
|
+
)
|
324
|
+
|
325
|
+
activate_keys.short_description = "🔓 Activate selected API keys"
|
326
|
+
|
327
|
+
def deactivate_keys(self, request, queryset):
|
328
|
+
"""Deactivate selected API keys."""
|
329
|
+
count = queryset.update(is_active=False)
|
330
|
+
self.message_user(
|
331
|
+
request,
|
332
|
+
f"Successfully deactivated {count} API keys.",
|
333
|
+
level='warning'
|
334
|
+
)
|
335
|
+
|
336
|
+
deactivate_keys.short_description = "🔒 Deactivate selected API keys"
|
337
|
+
|
338
|
+
def reset_usage(self, request, queryset):
|
339
|
+
"""Reset usage count for selected API keys."""
|
340
|
+
count = queryset.update(usage_count=0)
|
341
|
+
self.message_user(
|
342
|
+
request,
|
343
|
+
f"Successfully reset usage count for {count} API keys.",
|
344
|
+
level='info'
|
345
|
+
)
|
346
|
+
|
347
|
+
reset_usage.short_description = "🔄 Reset usage count"
|