django-cfg 1.3.7__py3-none-any.whl → 1.3.11__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 (246) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +269 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +183 -460
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +153 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
  48. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
  49. django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
  50. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
  51. django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
  52. django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
  53. django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
  54. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
  55. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  56. django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
  57. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
  58. django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
  59. django_cfg/apps/payments/config/__init__.py +14 -15
  60. django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
  61. django_cfg/apps/payments/config/helpers.py +8 -13
  62. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  63. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  64. django_cfg/apps/payments/middleware/api_access.py +32 -6
  65. django_cfg/apps/payments/migrations/0001_initial.py +33 -46
  66. django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
  67. django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
  68. django_cfg/apps/payments/models/balance.py +12 -0
  69. django_cfg/apps/payments/models/currencies.py +106 -32
  70. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  71. django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
  72. django_cfg/apps/payments/models/payments.py +94 -0
  73. django_cfg/apps/payments/services/core/base.py +4 -4
  74. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  75. django_cfg/apps/payments/services/core/payment_service.py +266 -39
  76. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  77. django_cfg/apps/payments/services/providers/base.py +303 -41
  78. django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
  79. django_cfg/apps/payments/services/providers/models/base.py +145 -0
  80. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  81. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  82. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  83. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  84. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  85. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  86. django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
  87. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  88. django_cfg/apps/payments/services/providers/registry.py +9 -37
  89. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  90. django_cfg/apps/payments/services/types/requests.py +19 -7
  91. django_cfg/apps/payments/signals/payment_signals.py +31 -2
  92. django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
  93. django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
  94. django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
  95. django_cfg/apps/payments/tasks/__init__.py +39 -0
  96. django_cfg/apps/payments/tasks/types.py +73 -0
  97. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  98. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  99. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  100. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  101. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  102. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  103. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  104. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  105. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  106. django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
  107. django_cfg/apps/payments/urls.py +3 -2
  108. django_cfg/apps/payments/urls_admin.py +1 -1
  109. django_cfg/apps/payments/views/api/currencies.py +8 -5
  110. django_cfg/apps/payments/views/overview/services.py +2 -2
  111. django_cfg/apps/payments/views/serializers/currencies.py +22 -8
  112. django_cfg/apps/support/admin/__init__.py +10 -1
  113. django_cfg/apps/support/admin/support_admin.py +338 -141
  114. django_cfg/apps/tasks/admin/__init__.py +11 -0
  115. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  116. django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
  117. django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
  118. django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
  119. django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
  120. django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
  121. django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
  122. django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
  123. django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
  124. django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
  125. django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
  126. django_cfg/apps/tasks/tasks/__init__.py +10 -0
  127. django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
  128. django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
  129. django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
  130. django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
  131. django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
  132. django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
  133. django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
  134. django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
  135. django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
  136. django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
  137. django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
  138. django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
  139. django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
  140. django_cfg/apps/tasks/urls.py +2 -2
  141. django_cfg/apps/tasks/urls_admin.py +2 -2
  142. django_cfg/apps/tasks/utils/__init__.py +1 -0
  143. django_cfg/apps/tasks/utils/simulator.py +356 -0
  144. django_cfg/apps/tasks/views/__init__.py +16 -0
  145. django_cfg/apps/tasks/views/api.py +569 -0
  146. django_cfg/apps/tasks/views/dashboard.py +58 -0
  147. django_cfg/config.py +1 -1
  148. django_cfg/core/config.py +10 -5
  149. django_cfg/core/generation.py +1 -1
  150. django_cfg/core/integration/__init__.py +21 -0
  151. django_cfg/management/commands/__init__.py +13 -1
  152. django_cfg/management/commands/migrate_all.py +9 -3
  153. django_cfg/management/commands/migrator.py +11 -6
  154. django_cfg/management/commands/rundramatiq.py +3 -2
  155. django_cfg/management/commands/rundramatiq_simulator.py +430 -0
  156. django_cfg/middleware/__init__.py +0 -2
  157. django_cfg/models/api_keys.py +115 -0
  158. django_cfg/models/constance.py +0 -11
  159. django_cfg/models/payments.py +137 -3
  160. django_cfg/modules/django_admin/__init__.py +64 -0
  161. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  162. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  163. django_cfg/modules/django_admin/decorators/display.py +106 -0
  164. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  165. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  166. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  167. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  168. django_cfg/modules/django_admin/models/__init__.py +20 -0
  169. django_cfg/modules/django_admin/models/action_models.py +33 -0
  170. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  171. django_cfg/modules/django_admin/models/base.py +26 -0
  172. django_cfg/modules/django_admin/models/display_models.py +31 -0
  173. django_cfg/modules/django_admin/utils/badges.py +159 -0
  174. django_cfg/modules/django_admin/utils/displays.py +247 -0
  175. django_cfg/modules/django_currency/__init__.py +2 -2
  176. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  177. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  178. django_cfg/modules/django_currency/core/converter.py +12 -12
  179. django_cfg/modules/django_currency/database/__init__.py +2 -2
  180. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  181. django_cfg/modules/django_llm/llm/client.py +10 -2
  182. django_cfg/modules/django_tasks.py +54 -21
  183. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  184. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  185. django_cfg/modules/django_unfold/dashboard.py +14 -13
  186. django_cfg/modules/django_unfold/models/config.py +1 -1
  187. django_cfg/registry/core.py +7 -9
  188. django_cfg/registry/third_party.py +2 -2
  189. django_cfg/template_archive/django_sample.zip +0 -0
  190. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -1
  191. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/RECORD +198 -160
  192. django_cfg/apps/accounts/admin/activity.py +0 -96
  193. django_cfg/apps/accounts/admin/group.py +0 -17
  194. django_cfg/apps/accounts/admin/otp.py +0 -59
  195. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  196. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  197. django_cfg/apps/accounts/admin/user.py +0 -300
  198. django_cfg/apps/agents/core/agent.py +0 -281
  199. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  200. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  201. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  202. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  203. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  204. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  205. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  206. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  207. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  208. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  209. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  210. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  211. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  212. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  213. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  214. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  215. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  216. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  217. django_cfg/apps/payments/config/constance/__init__.py +0 -22
  218. django_cfg/apps/payments/config/constance/config_service.py +0 -123
  219. django_cfg/apps/payments/config/constance/fields.py +0 -69
  220. django_cfg/apps/payments/config/constance/settings.py +0 -160
  221. django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
  222. django_cfg/apps/tasks/admin.py +0 -320
  223. django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
  224. django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
  225. django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
  226. django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
  227. django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
  228. django_cfg/apps/tasks/templates/tasks/base.html +0 -96
  229. django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
  230. django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
  231. django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
  232. django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
  233. django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
  234. django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
  235. django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
  236. django_cfg/apps/tasks/views.py +0 -461
  237. django_cfg/management/commands/auto_generate.py +0 -486
  238. django_cfg/middleware/static_nocache.py +0 -55
  239. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  240. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  241. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  242. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  243. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  244. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -1,631 +1,264 @@
1
1
  """
2
- API Key Admin interface with Unfold integration.
2
+ API Keys Admin interface using Django Admin Utilities.
3
3
 
4
- Advanced API key management with security features and monitoring.
4
+ Clean API key management with security features.
5
5
  """
6
6
 
7
7
  from django.contrib import admin
8
- from django.utils.html import format_html
9
- from django.contrib.humanize.templatetags.humanize import naturaltime
10
- from django.contrib import messages
11
- from django.shortcuts import redirect
12
- from django.utils.safestring import mark_safe
13
- from django.db.models import Count, Sum, Q
8
+ from django.db.models import Count, Q
14
9
  from django.utils import timezone
15
10
  from datetime import timedelta
16
- from typing import Optional
17
11
 
18
12
  from unfold.admin import ModelAdmin
19
- from unfold.decorators import display, action
20
- from unfold.enums import ActionVariant
21
13
 
22
- from ..models import APIKey
23
- from .filters import APIKeyStatusFilter, RecentActivityFilter
14
+ from django_cfg.modules.django_admin import (
15
+ OptimizedModelAdmin,
16
+ DisplayMixin,
17
+ UserDisplayConfig,
18
+ StatusBadgeConfig,
19
+ DateTimeDisplayConfig,
20
+ Icons,
21
+ display,
22
+ action,
23
+ ActionVariant
24
+ )
25
+ from django_cfg.modules.django_admin.utils.badges import StatusBadge
24
26
  from django_cfg.modules.django_logger import get_logger
25
27
 
28
+ from ..models import APIKey
29
+
26
30
  logger = get_logger("api_keys_admin")
27
31
 
28
32
 
29
33
  @admin.register(APIKey)
30
- class APIKeyAdmin(ModelAdmin):
34
+ class APIKeyAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin):
31
35
  """
32
- Advanced API Key admin with security features and monitoring.
36
+ APIKey admin using Django Admin Utilities.
33
37
 
34
38
  Features:
35
- - Security-focused key management
36
- - Usage monitoring and analytics
37
- - Expiration management and alerts
38
- - Bulk operations with audit trail
39
- - Key rotation and deactivation
39
+ - Secure API key display
40
+ - Usage tracking
41
+ - Activity monitoring
42
+ - Clean utilities integration
40
43
  """
41
44
 
42
- # Custom template for API key statistics
43
- change_list_template = 'admin/payments/apikey/change_list.html'
45
+ # Performance optimization
46
+ select_related_fields = ['user']
47
+ annotations = {}
48
+ # Note: Annotations disabled until proper usage tracking is implemented
49
+ # 'usage_count': Count('usage_logs') or similar
44
50
 
51
+ # List configuration
45
52
  list_display = [
46
53
  'key_display',
47
54
  'user_display',
48
55
  'name_display',
49
56
  'status_display',
50
57
  'usage_display',
51
- 'expiry_display',
52
- 'last_used_display',
53
- 'created_at_display'
58
+ 'created_display'
54
59
  ]
55
60
 
56
- list_display_links = ['key_display']
61
+ list_filter = [
62
+ 'is_active',
63
+ 'created_at',
64
+ 'last_used_at'
65
+ ]
57
66
 
58
67
  search_fields = [
59
68
  'name',
60
- 'user__email',
61
69
  'user__username',
70
+ 'user__email',
62
71
  'key' # Be careful with this in production
63
72
  ]
64
73
 
65
- list_filter = [
66
- APIKeyStatusFilter,
67
- RecentActivityFilter,
68
- 'is_active',
69
- 'created_at',
70
- 'expires_at'
71
- ]
72
-
73
74
  readonly_fields = [
74
75
  'key',
75
76
  'created_at',
76
77
  'updated_at',
77
- 'last_used_at'
78
+ 'last_used_at',
79
+ 'key_details_display'
78
80
  ]
79
81
 
80
- # Unfold actions
81
- actions_list = [
82
- 'deactivate_keys',
83
- 'extend_expiry',
84
- 'rotate_keys',
85
- 'send_expiry_alerts',
86
- 'export_usage_report'
87
- ]
82
+ # Register actions
83
+ actions = ['activate_selected_keys', 'deactivate_selected_keys', 'regenerate_selected_keys']
88
84
 
89
- fieldsets = [
90
- ('API Key Information', {
91
- 'fields': [
92
- 'user',
93
- 'name',
94
- 'key'
95
- ]
96
- }),
97
- ('Status & Security', {
98
- 'fields': [
99
- 'is_active',
100
- 'expires_at'
101
- ]
102
- }),
103
- ('Usage Statistics', {
104
- 'fields': [
105
- 'total_requests',
106
- 'last_used_at'
107
- ]
108
- }),
109
- ('Timestamps', {
110
- 'fields': ['created_at', 'updated_at'],
111
- 'classes': ['collapse']
112
- })
113
- ]
114
-
115
- def get_queryset(self, request):
116
- """Optimize queryset with user data."""
117
- return super().get_queryset(request).select_related('user')
118
-
119
- @display(description="API Key", ordering='key')
85
+ # Display methods using utilities
86
+ @display(description="API Key")
120
87
  def key_display(self, obj):
121
- """Display masked API key with copy functionality."""
122
- # Show only first 8 and last 4 characters for security
123
- masked_key = f"{obj.key[:8]}...{obj.key[-4:]}"
88
+ """Masked API key display for security with key icon."""
89
+ if not obj.key:
90
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.KEY_OFF)
91
+ return StatusBadge.create(text="No Key", variant="secondary", config=config)
124
92
 
125
- # Determine key status for styling
126
- if not obj.is_active:
127
- status_class = "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
128
- status_icon = "🔴"
129
- elif obj.expires_at and obj.expires_at <= timezone.now():
130
- status_class = "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"
131
- status_icon = "⌛"
132
- else:
133
- status_class = "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
134
- status_icon = "đŸŸĸ"
93
+ # Show first 8 and last 4 characters
94
+ masked_key = f"{obj.key[:8]}...{obj.key[-4:]}"
135
95
 
136
- return format_html(
137
- '<div class="flex items-center space-x-2">'
138
- '<span class="text-sm">{}</span>'
139
- '<span class="font-mono text-xs {} px-2 py-1 rounded" title="Click to copy full key">{}</span>'
140
- '</div>',
141
- status_icon,
142
- status_class,
143
- masked_key
96
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.KEY)
97
+ return StatusBadge.create(
98
+ text=masked_key,
99
+ variant="info",
100
+ config=config
144
101
  )
145
102
 
146
- @display(description="User", ordering='user__email')
103
+ @display(description="User", header=True)
147
104
  def user_display(self, obj):
148
- """Display user information with subscription status."""
149
- if obj.user:
150
- # Check if user has active subscription
151
- from ..models import Subscription
152
-
153
- active_subscription = Subscription.objects.filter(
154
- user=obj.user,
155
- status=Subscription.SubscriptionStatus.ACTIVE
156
- ).first()
157
-
158
- subscription_info = ""
159
- if active_subscription:
160
- subscription_info = format_html(
161
- '<div class="text-xs text-blue-600 dark:text-blue-400">{} tier</div>',
162
- active_subscription.tariff.tier.title()
163
- )
164
- else:
165
- subscription_info = format_html(
166
- '<div class="text-xs text-gray-500">No active subscription</div>'
167
- )
168
-
169
- return format_html(
170
- '<div>'
171
- '<div class="font-medium text-gray-900 dark:text-gray-100">{}</div>'
172
- '<div class="text-xs text-gray-500">{}</div>'
173
- '{}'
174
- '</div>',
175
- obj.user.get_full_name() or obj.user.username,
176
- obj.user.email,
177
- subscription_info
178
- )
179
- return format_html('<span class="text-gray-500">No user</span>')
105
+ """User display with avatar."""
106
+ return self.display_user_with_avatar(obj, 'user')
180
107
 
181
- @display(description="Name", ordering='name')
108
+ @display(description="Name")
182
109
  def name_display(self, obj):
183
- """Display API key name with truncation."""
184
- if len(obj.name) > 30:
185
- return format_html(
186
- '<span title="{}">{}</span>',
187
- obj.name,
188
- obj.name[:27] + "..."
189
- )
190
- return obj.name
110
+ """API key name display."""
111
+ if not obj.name:
112
+ return StatusBadge.create(text="Unnamed", variant="secondary")
113
+
114
+ return StatusBadge.create(
115
+ text=obj.name,
116
+ variant="primary"
117
+ )
191
118
 
192
- @display(description="Status")
119
+ @display(description="Status", label=True)
193
120
  def status_display(self, obj):
194
- """Display comprehensive status with multiple indicators."""
195
- badges = []
121
+ """Status display with activity level."""
122
+ if not obj.is_active:
123
+ return self.display_status_auto(
124
+ type('obj', (), {'status': 'Inactive'})(),
125
+ 'status',
126
+ StatusBadgeConfig(custom_mappings={'Inactive': 'danger'})
127
+ )
196
128
 
197
- # Active/Inactive status
198
- if obj.is_active:
199
- badges.append('<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">✅ Active</span>')
129
+ # Determine activity level based on last usage
130
+ if not obj.last_used_at:
131
+ status = "Active (Unused)"
132
+ variant = "info"
200
133
  else:
201
- badges.append('<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">❌ Inactive</span>')
202
-
203
- # Expiry status
204
- if obj.expires_at:
205
- now = timezone.now()
206
- if obj.expires_at <= now:
207
- badges.append('<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">⌛ Expired</span>')
208
- elif obj.expires_at <= now + timedelta(days=7):
209
- badges.append('<span class="inline-flex items-center rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-800 dark:bg-orange-900 dark:text-orange-200">âš ī¸ Expiring Soon</span>')
134
+ days_since_use = (timezone.now() - obj.last_used_at).days
135
+
136
+ if days_since_use <= 1:
137
+ status = "Active (Recent)"
138
+ variant = "success"
139
+ elif days_since_use <= 7:
140
+ status = "Active (This Week)"
141
+ variant = "success"
142
+ elif days_since_use <= 30:
143
+ status = "Active (This Month)"
144
+ variant = "warning"
145
+ else:
146
+ status = "Active (Idle)"
147
+ variant = "secondary"
210
148
 
211
- # Usage status
212
- if obj.total_requests == 0:
213
- badges.append('<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-900 dark:text-gray-200">🆕 Unused</span>')
214
- elif obj.total_requests >= 10000:
215
- badges.append('<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-200">đŸ”Ĩ Heavy Use</span>')
149
+ config = StatusBadgeConfig(
150
+ custom_mappings={status: variant},
151
+ show_icons=True
152
+ )
216
153
 
217
- return format_html('<div class="space-y-1">{}</div>', ''.join(badges))
154
+ return self.display_status_auto(
155
+ type('obj', (), {'status': status})(),
156
+ 'status',
157
+ config
158
+ )
218
159
 
219
160
  @display(description="Usage")
220
161
  def usage_display(self, obj):
221
- """Display usage statistics with visual indicators."""
222
- total_requests = obj.total_requests
223
-
224
- # Determine usage level and color
225
- if total_requests == 0:
226
- color = "text-gray-600 dark:text-gray-400"
227
- icon = "🆕"
228
- level = "Unused"
229
- elif total_requests < 100:
230
- color = "text-green-600 dark:text-green-400"
231
- icon = "đŸŸĸ"
232
- level = "Light"
233
- elif total_requests < 1000:
234
- color = "text-yellow-600 dark:text-yellow-400"
235
- icon = "🟡"
236
- level = "Moderate"
237
- elif total_requests < 10000:
238
- color = "text-orange-600 dark:text-orange-400"
239
- icon = "🟠"
240
- level = "Heavy"
241
- else:
242
- color = "text-red-600 dark:text-red-400"
243
- icon = "🔴"
244
- level = "Extreme"
245
-
246
- # Calculate recent usage (last 7 days)
247
- recent_threshold = timezone.now() - timedelta(days=7)
248
- # Note: This would require additional tracking in production
249
- # For now, we'll show total usage
250
-
251
- return format_html(
252
- '<div class="text-center">'
253
- '<div class="font-bold {} text-lg">'
254
- '<span class="mr-1">{}</span>{:,}'
255
- '</div>'
256
- '<div class="text-xs text-gray-500">{} usage</div>'
257
- '</div>',
258
- color,
259
- icon,
260
- total_requests,
261
- level
162
+ """Usage count display."""
163
+ # This would need actual usage tracking implementation
164
+ usage_count = getattr(obj, 'usage_count', 0)
165
+
166
+ return self.display_count_simple(
167
+ type('obj', (), {'usage_count': usage_count})(),
168
+ 'usage_count',
169
+ 'requests',
170
+ CounterBadgeConfig(use_humanize=True)
262
171
  )
263
172
 
264
- @display(description="Expiry", ordering='expires_at')
265
- def expiry_display(self, obj):
266
- """Display expiry information with countdown."""
267
- if not obj.expires_at:
268
- return format_html(
269
- '<div class="text-center text-blue-600 dark:text-blue-400">'
270
- '<div class="font-bold">∞</div>'
271
- '<div class="text-xs">Never expires</div>'
272
- '</div>'
273
- )
274
-
275
- now = timezone.now()
173
+ @display(description="Created")
174
+ def created_display(self, obj):
175
+ """Created time display."""
176
+ return self.display_datetime_relative(
177
+ obj,
178
+ 'created_at',
179
+ DateTimeDisplayConfig(show_relative=True, show_seconds=False)
180
+ )
181
+
182
+ # Readonly field displays
183
+ def key_details_display(self, obj):
184
+ """Detailed API key information for detail view."""
185
+ if not obj.pk:
186
+ return "Save to see details"
276
187
 
277
- if obj.expires_at <= now:
278
- # Already expired
279
- return format_html(
280
- '<div class="text-center text-red-600 dark:text-red-400">'
281
- '<div class="font-bold">Expired</div>'
282
- '<div class="text-xs">{}</div>'
283
- '</div>',
284
- naturaltime(obj.expires_at)
285
- )
188
+ from django.utils.html import format_html
189
+ from django_cfg.modules.django_admin.utils.displays import DateTimeDisplay
286
190
 
287
- time_remaining = obj.expires_at - now
191
+ details = []
288
192
 
289
- if time_remaining < timedelta(hours=24):
290
- color = "text-red-600 dark:text-red-400"
291
- icon = "🚨"
292
- elif time_remaining < timedelta(days=7):
293
- color = "text-orange-600 dark:text-orange-400"
294
- icon = "âš ī¸"
295
- else:
296
- color = "text-green-600 dark:text-green-400"
297
- icon = "✅"
193
+ # Full key (be careful in production!)
194
+ details.append(f"<strong>Full Key:</strong> <code>{obj.key}</code>")
298
195
 
299
- return format_html(
300
- '<div class="text-center {}">'
301
- '<div><span class="mr-1">{}</span>{}</div>'
302
- '<div class="text-xs">{}</div>'
303
- '</div>',
304
- color,
305
- icon,
306
- naturaltime(obj.expires_at),
307
- obj.expires_at.strftime('%Y-%m-%d')
308
- )
309
-
310
- @display(description="Last Used", ordering='last_used_at')
311
- def last_used_display(self, obj):
312
- """Display last usage with recency indicators."""
313
- if not obj.last_used_at:
314
- return format_html(
315
- '<div class="text-center text-gray-500">'
316
- '<div>Never</div>'
317
- '<div class="text-xs">🆕 Unused</div>'
318
- '</div>'
196
+ # Usage statistics
197
+ if obj.last_used_at:
198
+ last_used_html = DateTimeDisplay.relative(
199
+ obj.last_used_at,
200
+ DateTimeDisplayConfig(show_relative=True)
319
201
  )
202
+ details.append(f"<strong>Last Used:</strong> {last_used_html}")
203
+ else:
204
+ details.append("<strong>Last Used:</strong> Never")
320
205
 
321
- now = timezone.now()
322
- time_since_use = now - obj.last_used_at
206
+ # Age calculation
207
+ age = timezone.now() - obj.created_at
208
+ age_text = f"{age.days} days old"
209
+ details.append(f"<strong>Age:</strong> {age_text}")
323
210
 
324
- if time_since_use < timedelta(minutes=5):
325
- color = "text-green-600 dark:text-green-400"
326
- icon = "đŸŸĸ"
327
- status = "Just now"
328
- elif time_since_use < timedelta(hours=1):
329
- color = "text-green-600 dark:text-green-400"
330
- icon = "đŸŸĸ"
331
- status = "Recently"
332
- elif time_since_use < timedelta(days=1):
333
- color = "text-yellow-600 dark:text-yellow-400"
334
- icon = "🟡"
335
- status = "Today"
336
- elif time_since_use < timedelta(days=7):
337
- color = "text-orange-600 dark:text-orange-400"
338
- icon = "🟠"
339
- status = "This week"
211
+ # Security info
212
+ if obj.is_active:
213
+ security_status = '<span class="text-green-600">🔓 Active</span>'
340
214
  else:
341
- color = "text-red-600 dark:text-red-400"
342
- icon = "🔴"
343
- status = "Inactive"
215
+ security_status = '<span class="text-red-600">🔒 Inactive</span>'
344
216
 
345
- return format_html(
346
- '<div class="text-center {}">'
347
- '<div><span class="mr-1">{}</span>{}</div>'
348
- '<div class="text-xs">{}</div>'
349
- '</div>',
350
- color,
351
- icon,
352
- naturaltime(obj.last_used_at),
353
- status
354
- )
355
-
356
- @display(description="Created", ordering='created_at')
357
- def created_at_display(self, obj):
358
- """Display creation date."""
359
- return format_html(
360
- '<div class="text-xs">'
361
- '<div>{}</div>'
362
- '<div class="text-gray-500">{}</div>'
363
- '</div>',
364
- obj.created_at.strftime('%Y-%m-%d'),
365
- naturaltime(obj.created_at)
366
- )
367
-
368
- def changelist_view(self, request, extra_context=None):
369
- """Add API key statistics to changelist context."""
370
- extra_context = extra_context or {}
217
+ details.append(f"<strong>Security Status:</strong> {security_status}")
371
218
 
372
- try:
373
- # Basic statistics
374
- total_keys = APIKey.objects.count()
375
- active_keys = APIKey.objects.filter(is_active=True).count()
376
-
377
- # Expiry statistics
378
- now = timezone.now()
379
- expired_keys = APIKey.objects.filter(expires_at__lte=now).count()
380
- expiring_soon = APIKey.objects.filter(
381
- expires_at__lte=now + timedelta(days=7),
382
- expires_at__gt=now
383
- ).count()
384
-
385
- # Usage statistics
386
- total_requests = APIKey.objects.aggregate(
387
- total=Sum('total_requests')
388
- )['total'] or 0
389
-
390
- unused_keys = APIKey.objects.filter(total_requests=0).count()
391
- heavy_usage_keys = APIKey.objects.filter(total_requests__gte=10000).count()
392
-
393
- # Recent activity
394
- recent_threshold = timezone.now() - timedelta(days=7)
395
- recently_used = APIKey.objects.filter(
396
- last_used_at__gte=recent_threshold
397
- ).count()
398
-
399
- # Security alerts
400
- never_used_old_keys = APIKey.objects.filter(
401
- total_requests=0,
402
- created_at__lte=timezone.now() - timedelta(days=30)
403
- ).count()
404
-
405
- # Top users by API key count
406
- top_users = APIKey.objects.values(
407
- 'user__email', 'user__username'
408
- ).annotate(
409
- key_count=Count('id'),
410
- total_usage=Sum('total_requests')
411
- ).order_by('-key_count')[:5]
412
-
413
- extra_context.update({
414
- 'api_key_stats': {
415
- 'total_keys': total_keys,
416
- 'active_keys': active_keys,
417
- 'expired_keys': expired_keys,
418
- 'expiring_soon': expiring_soon,
419
- 'total_requests': total_requests,
420
- 'unused_keys': unused_keys,
421
- 'heavy_usage_keys': heavy_usage_keys,
422
- 'recently_used': recently_used,
423
- 'never_used_old_keys': never_used_old_keys,
424
- 'top_users': top_users,
425
- }
426
- })
427
-
428
- except Exception as e:
429
- logger.warning(f"Failed to generate API key statistics: {e}")
430
- extra_context['api_key_stats'] = None
431
-
432
- return super().changelist_view(request, extra_context)
219
+ return format_html("<br>".join(details))
433
220
 
434
- # ===== ADMIN ACTIONS =====
221
+ key_details_display.short_description = "Key Details"
435
222
 
436
- @action(
437
- description="🔒 Deactivate Keys",
438
- icon="block",
439
- variant=ActionVariant.WARNING
440
- )
223
+ # Actions
224
+ @action(description="Activate selected keys", variant=ActionVariant.SUCCESS)
225
+ def activate_keys(self, request, queryset):
226
+ """Activate selected API keys."""
227
+ updated = queryset.update(is_active=True)
228
+ self.message_user(
229
+ request,
230
+ f"Successfully activated {updated} API key(s).",
231
+ level='SUCCESS'
232
+ )
233
+
234
+ @action(description="Deactivate selected keys", variant=ActionVariant.WARNING)
441
235
  def deactivate_keys(self, request, queryset):
442
236
  """Deactivate selected API keys."""
443
-
444
- active_keys = queryset.filter(is_active=True)
445
- deactivated_count = 0
446
-
447
- for api_key in active_keys:
448
- try:
449
- api_key.deactivate(reason=f"Deactivated by admin {request.user.username}")
450
- deactivated_count += 1
451
-
452
- except Exception as e:
453
- logger.error(f"Failed to deactivate API key {api_key.id}: {e}")
454
-
455
- if deactivated_count > 0:
456
- messages.success(
457
- request,
458
- f"🔒 Deactivated {deactivated_count} API keys"
459
- )
460
- messages.info(
461
- request,
462
- "â„šī¸ Deactivated keys can be reactivated if needed"
463
- )
464
-
465
- skipped = queryset.count() - deactivated_count
466
- if skipped > 0:
467
- messages.info(
468
- request,
469
- f"â„šī¸ Skipped {skipped} keys (already inactive)"
470
- )
471
-
472
- @action(
473
- description="📅 Extend Expiry (30 days)",
474
- icon="schedule",
475
- variant=ActionVariant.INFO
476
- )
477
- def extend_expiry(self, request, queryset):
478
- """Extend expiry of selected API keys by 30 days."""
479
-
480
- extended_count = 0
481
-
482
- for api_key in queryset:
483
- try:
484
- api_key.extend_expiry(days=30)
485
- extended_count += 1
486
-
487
- except Exception as e:
488
- logger.error(f"Failed to extend API key {api_key.id} expiry: {e}")
489
-
490
- if extended_count > 0:
491
- messages.success(
492
- request,
493
- f"📅 Extended expiry for {extended_count} API keys by 30 days"
494
- )
495
-
496
- @action(
497
- description="🔄 Rotate Keys",
498
- icon="refresh",
499
- variant=ActionVariant.WARNING
500
- )
501
- def rotate_keys(self, request, queryset):
502
- """Rotate selected API keys (generate new keys)."""
503
-
504
- rotated_count = 0
505
-
506
- for api_key in queryset:
507
- try:
508
- # Generate new key
509
- old_key = api_key.key
510
- api_key.generate_key()
511
- api_key.save()
512
-
513
- rotated_count += 1
514
-
515
- logger.info(
516
- f"API key rotated for user {api_key.user.email}",
517
- extra={
518
- 'api_key_id': str(api_key.id),
519
- 'user_id': api_key.user.id,
520
- 'old_key_prefix': old_key[:8],
521
- 'new_key_prefix': api_key.key[:8],
522
- 'rotated_by': request.user.username
523
- }
524
- )
525
-
526
- except Exception as e:
527
- logger.error(f"Failed to rotate API key {api_key.id}: {e}")
528
-
529
- if rotated_count > 0:
530
- messages.success(
531
- request,
532
- f"🔄 Rotated {rotated_count} API keys"
533
- )
534
- messages.warning(
535
- request,
536
- "âš ī¸ Users will need to update their applications with new keys!"
537
- )
538
-
539
- @action(
540
- description="🔔 Send Expiry Alerts",
541
- icon="notifications",
542
- variant=ActionVariant.INFO
543
- )
544
- def send_expiry_alerts(self, request, queryset):
545
- """Send expiry alerts for keys expiring soon."""
546
-
547
- now = timezone.now()
548
- expiring_keys = queryset.filter(
549
- is_active=True,
550
- expires_at__lte=now + timedelta(days=7),
551
- expires_at__gt=now
237
+ updated = queryset.update(is_active=False)
238
+ self.message_user(
239
+ request,
240
+ f"Successfully deactivated {updated} API key(s).",
241
+ level='WARNING'
552
242
  )
553
-
554
- alert_count = 0
555
-
556
- for api_key in expiring_keys:
557
- try:
558
- # In production, this would send an actual notification
559
- logger.info(
560
- f"Expiry alert for API key {api_key.name}",
561
- extra={
562
- 'api_key_id': str(api_key.id),
563
- 'user_email': api_key.user.email,
564
- 'expires_at': api_key.expires_at.isoformat()
565
- }
566
- )
567
- alert_count += 1
568
-
569
- except Exception as e:
570
- logger.error(f"Failed to send alert for API key {api_key.id}: {e}")
571
-
572
- if alert_count > 0:
573
- messages.success(
574
- request,
575
- f"🔔 Sent expiry alerts for {alert_count} API keys"
576
- )
577
- else:
578
- messages.info(
579
- request,
580
- "â„šī¸ No API keys expiring soon in selection"
581
- )
582
243
 
583
- @action(
584
- description="📊 Export Usage Report",
585
- icon="download",
586
- variant=ActionVariant.INFO
587
- )
588
- def export_usage_report(self, request, queryset):
589
- """Export API key usage report to CSV."""
590
-
591
- import csv
592
- from django.http import HttpResponse
593
-
594
- response = HttpResponse(content_type='text/csv')
595
- response['Content-Disposition'] = f'attachment; filename="api_keys_usage_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"'
596
-
597
- writer = csv.writer(response)
598
- writer.writerow([
599
- 'Key Name', 'User Email', 'User Name', 'Total Requests', 'Is Active',
600
- 'Created', 'Last Used', 'Expires', 'Status'
601
- ])
244
+ @action(description="Regenerate selected keys", variant=ActionVariant.DANGER)
245
+ def regenerate_keys(self, request, queryset):
246
+ """Regenerate selected API keys."""
247
+ import secrets
248
+ import string
602
249
 
250
+ updated_count = 0
603
251
  for api_key in queryset:
604
- # Determine status
605
- if not api_key.is_active:
606
- status = 'Inactive'
607
- elif api_key.expires_at and api_key.expires_at <= timezone.now():
608
- status = 'Expired'
609
- elif api_key.total_requests == 0:
610
- status = 'Unused'
611
- else:
612
- status = 'Active'
252
+ # Generate new key
253
+ alphabet = string.ascii_letters + string.digits
254
+ new_key = ''.join(secrets.choice(alphabet) for _ in range(32))
613
255
 
614
- writer.writerow([
615
- api_key.name,
616
- api_key.user.email if api_key.user else '',
617
- api_key.user.get_full_name() if api_key.user else '',
618
- api_key.total_requests,
619
- 'Yes' if api_key.is_active else 'No',
620
- api_key.created_at.isoformat(),
621
- api_key.last_used_at.isoformat() if api_key.last_used_at else '',
622
- api_key.expires_at.isoformat() if api_key.expires_at else 'Never',
623
- status
624
- ])
256
+ api_key.key = new_key
257
+ api_key.save()
258
+ updated_count += 1
625
259
 
626
- messages.success(
260
+ self.message_user(
627
261
  request,
628
- f"📊 Exported usage report for {queryset.count()} API keys"
262
+ f"Successfully regenerated {updated_count} API key(s).",
263
+ level='WARNING'
629
264
  )
630
-
631
- return response