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.
Files changed (125) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.2.22"
35
+ __version__ = "1.2.25"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -23,7 +23,7 @@ User = get_user_model()
23
23
 
24
24
 
25
25
  @dramatiq.actor(
26
- queue_name="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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="knowledge",
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"