django-cfg 1.2.29__py3-none-any.whl → 1.3.1__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 (258) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -9
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +600 -108
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +470 -64
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
  39. django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
  43. django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +16 -20
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +207 -67
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -284
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +344 -468
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -484
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +232 -71
  74. django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
  75. django_cfg/apps/payments/services/providers/registry.py +429 -80
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +211 -130
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +129 -98
  85. django_cfg/apps/payments/signals/subscription_signals.py +195 -143
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +46 -47
  93. django_cfg/apps/payments/urls_admin.py +49 -0
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/apps/tasks/urls.py +0 -2
  110. django_cfg/apps/tasks/urls_admin.py +14 -0
  111. django_cfg/apps/urls.py +4 -4
  112. django_cfg/config.py +1 -1
  113. django_cfg/core/config.py +75 -4
  114. django_cfg/core/generation.py +25 -4
  115. django_cfg/core/integration/README.md +363 -0
  116. django_cfg/core/integration/__init__.py +47 -0
  117. django_cfg/core/integration/commands_collector.py +239 -0
  118. django_cfg/core/integration/display/__init__.py +15 -0
  119. django_cfg/core/integration/display/base.py +157 -0
  120. django_cfg/core/integration/display/ngrok.py +164 -0
  121. django_cfg/core/integration/display/startup.py +815 -0
  122. django_cfg/core/integration/url_integration.py +123 -0
  123. django_cfg/core/integration/version_checker.py +160 -0
  124. django_cfg/management/commands/auto_generate.py +4 -0
  125. django_cfg/management/commands/check_settings.py +6 -0
  126. django_cfg/management/commands/clear_constance.py +5 -2
  127. django_cfg/management/commands/create_token.py +6 -0
  128. django_cfg/management/commands/list_urls.py +6 -0
  129. django_cfg/management/commands/migrate_all.py +6 -0
  130. django_cfg/management/commands/migrator.py +3 -0
  131. django_cfg/management/commands/rundramatiq.py +6 -0
  132. django_cfg/management/commands/runserver_ngrok.py +51 -29
  133. django_cfg/management/commands/script.py +6 -0
  134. django_cfg/management/commands/show_config.py +12 -2
  135. django_cfg/management/commands/show_urls.py +4 -0
  136. django_cfg/management/commands/superuser.py +6 -0
  137. django_cfg/management/commands/task_clear.py +4 -1
  138. django_cfg/management/commands/task_status.py +3 -1
  139. django_cfg/management/commands/test_email.py +3 -0
  140. django_cfg/management/commands/test_telegram.py +6 -0
  141. django_cfg/management/commands/test_twilio.py +6 -0
  142. django_cfg/management/commands/tree.py +6 -0
  143. django_cfg/management/commands/validate_config.py +155 -149
  144. django_cfg/models/constance.py +31 -11
  145. django_cfg/models/payments.py +175 -498
  146. django_cfg/modules/django_currency/__init__.py +16 -11
  147. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  148. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  149. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  150. django_cfg/modules/django_currency/core/__init__.py +1 -7
  151. django_cfg/modules/django_currency/core/converter.py +18 -23
  152. django_cfg/modules/django_currency/core/models.py +122 -11
  153. django_cfg/modules/django_currency/database/__init__.py +4 -4
  154. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  155. django_cfg/modules/django_logger.py +160 -146
  156. django_cfg/modules/django_unfold/dashboard.py +65 -12
  157. django_cfg/registry/core.py +1 -0
  158. django_cfg/template_archive/django_sample.zip +0 -0
  159. django_cfg/templates/admin/components/action_grid.html +9 -9
  160. django_cfg/templates/admin/components/metric_card.html +5 -5
  161. django_cfg/templates/admin/components/status_badge.html +2 -2
  162. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  163. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  164. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  165. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  166. django_cfg/utils/smart_defaults.py +222 -571
  167. django_cfg/utils/toolkit.py +51 -11
  168. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
  169. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
  170. django_cfg/apps/payments/__init__.py +0 -8
  171. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  172. django_cfg/apps/payments/config/module.py +0 -70
  173. django_cfg/apps/payments/config/providers.py +0 -105
  174. django_cfg/apps/payments/config/settings.py +0 -96
  175. django_cfg/apps/payments/config/utils.py +0 -52
  176. django_cfg/apps/payments/decorators.py +0 -291
  177. django_cfg/apps/payments/management/commands/README.md +0 -178
  178. django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
  179. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  180. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  181. django_cfg/apps/payments/managers/__init__.py +0 -22
  182. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  183. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  184. django_cfg/apps/payments/managers/currency_manager.py +0 -83
  185. django_cfg/apps/payments/managers/payment_manager.py +0 -44
  186. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  187. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  188. django_cfg/apps/payments/models/events.py +0 -73
  189. django_cfg/apps/payments/serializers/__init__.py +0 -56
  190. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  191. django_cfg/apps/payments/serializers/balance.py +0 -59
  192. django_cfg/apps/payments/serializers/currencies.py +0 -55
  193. django_cfg/apps/payments/serializers/payments.py +0 -62
  194. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  195. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  196. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  197. django_cfg/apps/payments/services/cache/base.py +0 -30
  198. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  199. django_cfg/apps/payments/services/internal_types.py +0 -297
  200. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  201. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  202. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
  203. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  204. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  205. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  206. django_cfg/apps/payments/services/security/__init__.py +0 -34
  207. django_cfg/apps/payments/services/security/error_handler.py +0 -637
  208. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  209. django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
  210. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  211. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  212. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  213. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  214. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  215. django_cfg/apps/payments/tasks/__init__.py +0 -12
  216. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  217. django_cfg/apps/payments/templates/payments/base.html +0 -182
  218. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  219. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  220. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
  221. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  222. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
  223. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
  224. django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
  225. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  226. django_cfg/apps/payments/urls_templates.py +0 -52
  227. django_cfg/apps/payments/utils/__init__.py +0 -45
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -245
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -62
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -111
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -312
  241. django_cfg/apps/payments/views/templates/base.py +0 -204
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -164
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -240
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -65
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  252. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  253. django_cfg/template_archive/.gitignore +0 -1
  254. django_cfg/template_archive/__init__.py +0 -0
  255. django_cfg/urls.py +0 -33
  256. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  257. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  258. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,50 +1,102 @@
1
1
  """
2
- Admin interface for currencies.
2
+ Currency Admin interfaces with Unfold integration.
3
+
4
+ Includes universal currency/rate update functionality and modern UI.
3
5
  """
4
6
 
5
7
  from django.contrib import admin
6
8
  from django.utils.html import format_html
7
9
  from django.contrib.humanize.templatetags.humanize import naturaltime
8
- from unfold.admin import ModelAdmin
9
- from unfold.decorators import display
10
+ from django.contrib import messages
11
+ from django.shortcuts import redirect
12
+ from django.core.management import call_command
13
+ from django.utils.safestring import mark_safe
14
+ from django.db.models import Count, Q
15
+ from django.utils import timezone
16
+ from datetime import timedelta
17
+ import threading
18
+ from typing import Optional
19
+
20
+ from unfold.admin import ModelAdmin, TabularInline
21
+ from unfold.decorators import display, action
22
+ from unfold.enums import ActionVariant
10
23
 
11
- from ..models import Currency, CurrencyNetwork
12
- from .filters import CurrencyTypeFilter
24
+ from ..models import Currency, Network, ProviderCurrency
25
+ from .filters import CurrencyTypeFilter, CurrencyRateStatusFilter
26
+ from django_cfg.modules.django_logger import get_logger
27
+
28
+ logger = get_logger("currencies_admin")
13
29
 
14
30
 
15
31
  @admin.register(Currency)
16
32
  class CurrencyAdmin(ModelAdmin):
17
- """Admin interface for currencies."""
33
+ """
34
+ Modern Currency admin with Unfold styling and universal update functionality.
35
+
36
+ Features:
37
+ - Real-time USD rate display with freshness indicators
38
+ - Universal update button (populate + sync + rates)
39
+ - Advanced filtering and search
40
+ - Provider count statistics
41
+ - Integration with django_currency module
42
+ """
43
+
44
+ # Custom template for statistics dashboard
45
+ change_list_template = 'admin/payments/currency/change_list.html'
18
46
 
19
47
  list_display = [
20
- 'currency_display',
21
- 'type_display',
22
- 'rate_display',
23
- 'status_display',
48
+ 'code_display',
49
+ 'name_display',
50
+ 'currency_type_badge',
51
+ 'usd_rate_display',
52
+ 'provider_count_badge',
53
+ 'rate_freshness',
24
54
  'created_at_display'
25
55
  ]
26
56
 
27
- list_display_links = ['currency_display']
57
+ list_display_links = ['code_display']
28
58
 
29
- search_fields = ['code', 'name', 'symbol']
59
+ search_fields = [
60
+ 'code',
61
+ 'name',
62
+ 'symbol'
63
+ ]
30
64
 
31
65
  list_filter = [
32
66
  CurrencyTypeFilter,
67
+ CurrencyRateStatusFilter,
33
68
  'is_active',
34
69
  'created_at'
35
70
  ]
36
71
 
37
- readonly_fields = ['rate_updated_at', 'created_at', 'updated_at']
72
+ readonly_fields = [
73
+ 'created_at',
74
+ 'updated_at',
75
+ 'exchange_rate_source'
76
+ ]
77
+
78
+ # Unfold actions
79
+ actions_list = [
80
+ 'universal_update_all',
81
+ 'update_selected_rates',
82
+ 'sync_provider_currencies'
83
+ ]
38
84
 
39
85
  fieldsets = [
40
86
  ('Currency Information', {
41
- 'fields': ['code', 'name', 'symbol', 'currency_type']
42
- }),
43
- ('Configuration', {
44
- 'fields': ['decimal_places', 'min_payment_amount', 'is_active']
87
+ 'fields': [
88
+ 'code',
89
+ 'name',
90
+ 'currency_type',
91
+ 'symbol',
92
+ 'decimal_places'
93
+ ]
45
94
  }),
46
- ('Exchange Rate', {
47
- 'fields': ['usd_rate', 'rate_updated_at']
95
+ ('Status & Configuration', {
96
+ 'fields': [
97
+ 'is_active',
98
+ 'exchange_rate_source'
99
+ ]
48
100
  }),
49
101
  ('Timestamps', {
50
102
  'fields': ['created_at', 'updated_at'],
@@ -52,135 +104,575 @@ class CurrencyAdmin(ModelAdmin):
52
104
  })
53
105
  ]
54
106
 
55
- @display(description="Currency")
56
- def currency_display(self, obj):
57
- """Display currency with symbol."""
107
+ def get_queryset(self, request):
108
+ """Optimize queryset with provider count annotation."""
109
+ return super().get_queryset(request).annotate(
110
+ provider_count=Count('provider_configs')
111
+ ).select_related()
112
+
113
+ @display(description="Code", ordering='code')
114
+ def code_display(self, obj):
115
+ """Display currency code with symbol."""
116
+ if obj.symbol:
117
+ return format_html(
118
+ '<span class="font-mono font-bold text-primary-600 dark:text-primary-400">{}</span> '
119
+ '<span class="text-gray-500 text-sm">{}</span>',
120
+ obj.code,
121
+ obj.symbol
122
+ )
58
123
  return format_html(
59
- '<strong>{}</strong> {}<br><small>{}</small>',
60
- obj.code,
61
- obj.symbol,
62
- obj.name
124
+ '<span class="font-mono font-bold text-primary-600 dark:text-primary-400">{}</span>',
125
+ obj.code
63
126
  )
64
127
 
65
- @display(description="Type")
66
- def type_display(self, obj):
67
- """Display currency type with badge."""
68
- type_colors = {
69
- 'fiat': '#28a745',
70
- 'crypto': '#fd7e14',
71
- }
128
+ @display(description="Name", ordering='name')
129
+ def name_display(self, obj):
130
+ """Display currency name with truncation."""
131
+ if len(obj.name) > 25:
132
+ return format_html(
133
+ '<span title="{}">{}</span>',
134
+ obj.name,
135
+ obj.name[:22] + "..."
136
+ )
137
+ return obj.name
138
+
139
+ @display(description="Type", ordering='currency_type')
140
+ def currency_type_badge(self, obj):
141
+ """Display currency type with colored badge."""
142
+ if obj.currency_type == Currency.CurrencyType.FIAT:
143
+ return format_html(
144
+ '<span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">'
145
+ '💰 Fiat'
146
+ '</span>'
147
+ )
148
+ else:
149
+ return format_html(
150
+ '<span class="inline-flex items-center rounded-full bg-orange-100 px-2.5 py-0.5 text-xs font-medium text-orange-800 dark:bg-orange-900 dark:text-orange-200">'
151
+ '₿ Crypto'
152
+ '</span>'
153
+ )
154
+
155
+ @display(description="USD Rate", ordering='provider_configs__usd_rate')
156
+ def usd_rate_display(self, obj):
157
+ """Display USD rate with freshness indicator."""
158
+ # Get the most recent rate from ProviderCurrency
159
+ provider_currency = obj.provider_configs_set.filter(
160
+ usd_rate__isnull=False
161
+ ).order_by('-updated_at').first()
72
162
 
73
- color = type_colors.get(obj.currency_type, '#6c757d')
163
+ if not provider_currency or not provider_currency.usd_rate:
164
+ return format_html(
165
+ '<span class="text-red-500 text-sm">❌ No rate</span>'
166
+ )
74
167
 
75
- return format_html(
76
- '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>',
77
- color,
78
- obj.get_currency_type_display()
168
+ # Check freshness (24 hours)
169
+ is_fresh = (
170
+ provider_currency.updated_at and
171
+ timezone.now() - provider_currency.updated_at < timedelta(hours=24)
79
172
  )
80
-
81
- @display(description="USD Rate")
82
- def rate_display(self, obj):
83
- """Display exchange rate."""
84
- if obj.usd_rate != 1.0:
173
+
174
+ color_class = "text-green-600 dark:text-green-400" if is_fresh else "text-orange-600 dark:text-orange-400"
175
+ icon = "🟢" if is_fresh else "🟠"
176
+
177
+ if obj.currency_type == Currency.CurrencyType.FIAT:
178
+ # Fiat: show 1 USD = X CURRENCY
179
+ tokens_per_usd = 1.0 / float(provider_currency.usd_rate) if provider_currency.usd_rate > 0 else 0
85
180
  return format_html(
86
- '<strong>1 {} = ${:.6f}</strong><br><small>Updated: {}</small>',
181
+ '<div class="{}">{} $1 = {} {}</div>'
182
+ '<small class="text-xs text-gray-500">Updated: {}</small>',
183
+ color_class,
184
+ icon,
185
+ f"{tokens_per_usd:.4f}",
87
186
  obj.code,
88
- obj.usd_rate,
89
- naturaltime(obj.rate_updated_at) if obj.rate_updated_at else 'Never'
187
+ naturaltime(provider_currency.updated_at) if provider_currency.updated_at else "Never"
188
+ )
189
+ else:
190
+ # Crypto: show 1 CURRENCY = X USD
191
+ usd_rate = float(provider_currency.usd_rate)
192
+ if usd_rate > 1:
193
+ rate_display = f"${usd_rate:,.2f}"
194
+ elif usd_rate > 0.01:
195
+ rate_display = f"${usd_rate:.4f}"
196
+ else:
197
+ rate_display = f"${usd_rate:.8f}"
198
+
199
+ return format_html(
200
+ '<div class="{}">{} 1 {} = {}</div>'
201
+ '<small class="text-xs text-gray-500">Updated: {}</small>',
202
+ color_class,
203
+ icon,
204
+ obj.code,
205
+ rate_display,
206
+ naturaltime(provider_currency.updated_at) if provider_currency.updated_at else "Never"
90
207
  )
91
- return format_html('<span style="color: #6c757d;">Base currency</span>')
92
208
 
93
- @display(description="Status")
94
- def status_display(self, obj):
95
- """Display status badge."""
96
- if obj.is_active:
209
+ @display(description="Providers")
210
+ def provider_count_badge(self, obj):
211
+ """Display provider count with badge."""
212
+ count = getattr(obj, 'provider_count', 0)
213
+ if count > 0:
97
214
  return format_html(
98
- '<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Active</span>'
215
+ '<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">'
216
+ '{} provider{}'
217
+ '</span>',
218
+ count,
219
+ 's' if count != 1 else ''
99
220
  )
221
+ return format_html(
222
+ '<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-900 dark:text-gray-200">'
223
+ 'No providers'
224
+ '</span>'
225
+ )
226
+
227
+ @display(description="Rate Status")
228
+ def rate_freshness(self, obj):
229
+ """Display rate freshness indicator."""
230
+ provider_currency = obj.provider_configs_set.filter(
231
+ usd_rate__isnull=False
232
+ ).order_by('-updated_at').first()
233
+
234
+ if not provider_currency or not provider_currency.updated_at:
235
+ return format_html('<span class="text-red-500">❌ Never</span>')
236
+
237
+ age = timezone.now() - provider_currency.updated_at
238
+
239
+ if age < timedelta(hours=1):
240
+ return format_html('<span class="text-green-500">🟢 Fresh</span>')
241
+ elif age < timedelta(hours=24):
242
+ return format_html('<span class="text-yellow-500">🟡 Recent</span>')
243
+ elif age < timedelta(days=7):
244
+ return format_html('<span class="text-orange-500">🟠 Stale</span>')
100
245
  else:
246
+ return format_html('<span class="text-red-500">🔴 Old</span>')
247
+
248
+ @display(description="Created", ordering='created_at')
249
+ def created_at_display(self, obj):
250
+ """Display creation date."""
251
+ return naturaltime(obj.created_at)
252
+
253
+ def changelist_view(self, request, extra_context=None):
254
+ """Add statistics to changelist context."""
255
+ extra_context = extra_context or {}
256
+
257
+ try:
258
+ # Basic statistics
259
+ total_currencies = Currency.objects.count()
260
+ fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
261
+ crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
262
+ active_count = Currency.objects.filter(is_active=True).count()
263
+
264
+ # Provider statistics
265
+ total_provider_currencies = ProviderCurrency.objects.count()
266
+ enabled_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
267
+
268
+ # Rate statistics
269
+ currencies_with_rates = Currency.objects.filter(
270
+ provider_configs__usd_rate__isnull=False
271
+ ).distinct().count()
272
+ rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
273
+
274
+ # Fresh rates (updated in last 24 hours)
275
+ fresh_threshold = timezone.now() - timedelta(hours=24)
276
+ fresh_rates_count = Currency.objects.filter(
277
+ provider_configs__updated_at__gte=fresh_threshold
278
+ ).distinct().count()
279
+
280
+ # Top currencies by provider count
281
+ top_currencies = Currency.objects.annotate(
282
+ provider_count=Count('provider_configs')
283
+ ).filter(provider_count__gt=0).order_by('-provider_count')[:5]
284
+
285
+ extra_context.update({
286
+ 'currency_stats': {
287
+ 'total_currencies': total_currencies,
288
+ 'fiat_count': fiat_count,
289
+ 'crypto_count': crypto_count,
290
+ 'active_count': active_count,
291
+ 'total_provider_currencies': total_provider_currencies,
292
+ 'enabled_provider_currencies': enabled_provider_currencies,
293
+ 'currencies_with_rates': currencies_with_rates,
294
+ 'rate_coverage': rate_coverage,
295
+ 'fresh_rates_count': fresh_rates_count,
296
+ 'top_currencies': top_currencies,
297
+ }
298
+ })
299
+
300
+ except Exception as e:
301
+ logger.warning(f"Failed to generate currency statistics: {e}")
302
+ extra_context['currency_stats'] = None
303
+
304
+ return super().changelist_view(request, extra_context)
305
+
306
+ # ===== ADMIN ACTIONS =====
307
+
308
+ @action(
309
+ description="🚀 Universal Update (All)",
310
+ icon="sync",
311
+ variant=ActionVariant.SUCCESS,
312
+ url_path="universal-update"
313
+ )
314
+ def universal_update_all(self, request):
315
+ """
316
+ Universal update: populate currencies + sync providers + update rates.
317
+
318
+ This is the main action that performs a complete system update.
319
+ """
320
+ try:
321
+ def background_update():
322
+ """Background task for comprehensive update."""
323
+ try:
324
+ logger.info("Starting universal currency update")
325
+
326
+ # 1. Populate missing currencies (fast)
327
+ call_command('manage_currencies', '--populate', '--skip-existing')
328
+
329
+ # 2. Sync all providers (medium speed)
330
+ call_command('manage_providers', '--all')
331
+
332
+ # 3. Update USD rates (slower)
333
+ call_command('manage_currencies', '--rates-only')
334
+
335
+ logger.info("Universal currency update completed successfully")
336
+
337
+ except Exception as e:
338
+ logger.error(f"Universal update failed: {e}")
339
+
340
+ # Start background update
341
+ thread = threading.Thread(target=background_update)
342
+ thread.daemon = True
343
+ thread.start()
344
+
345
+ # Generate immediate statistics for user feedback
346
+ stats = self._get_current_stats()
347
+
348
+ success_message = self._generate_update_message(stats)
349
+ messages.success(request, mark_safe(success_message))
350
+
351
+ logger.info(f"Universal update initiated by user {request.user.username}")
352
+
353
+ except Exception as e:
354
+ error_msg = f"❌ Failed to start universal update: {str(e)}"
355
+ messages.error(request, error_msg)
356
+ logger.error(f"Universal update initiation failed: {e}")
357
+
358
+ return redirect(request.META.get('HTTP_REFERER', '/admin/payments/currency/'))
359
+
360
+ @action(
361
+ description="💱 Update Selected Rates",
362
+ icon="trending_up",
363
+ variant=ActionVariant.WARNING
364
+ )
365
+ def update_selected_rates(self, request, queryset):
366
+ """Update USD rates for selected currencies only."""
367
+ try:
368
+ currency_codes = list(queryset.values_list('code', flat=True))
369
+
370
+ def background_rate_update():
371
+ """Background task for rate updates."""
372
+ try:
373
+ for code in currency_codes:
374
+ call_command('manage_currencies', '--currency', code, '--rates-only')
375
+ except Exception as e:
376
+ logger.error(f"Selected rate update failed: {e}")
377
+
378
+ thread = threading.Thread(target=background_rate_update)
379
+ thread.daemon = True
380
+ thread.start()
381
+
382
+ messages.success(
383
+ request,
384
+ f"💱 Started rate update for {len(currency_codes)} currencies: {', '.join(currency_codes[:5])}"
385
+ f"{'...' if len(currency_codes) > 5 else ''}"
386
+ )
387
+
388
+ except Exception as e:
389
+ messages.error(request, f"❌ Failed to update rates: {str(e)}")
390
+
391
+ @action(
392
+ description="🔄 Sync Provider Currencies",
393
+ icon="cloud_sync",
394
+ variant=ActionVariant.INFO
395
+ )
396
+ def sync_provider_currencies(self, request, queryset):
397
+ """Sync provider currencies for selected base currencies."""
398
+ try:
399
+ currency_codes = list(queryset.values_list('code', flat=True))
400
+
401
+ def background_sync():
402
+ """Background task for provider sync."""
403
+ try:
404
+ call_command('manage_providers', '--all', '--currencies', ','.join(currency_codes))
405
+ except Exception as e:
406
+ logger.error(f"Provider sync failed: {e}")
407
+
408
+ thread = threading.Thread(target=background_sync)
409
+ thread.daemon = True
410
+ thread.start()
411
+
412
+ messages.success(
413
+ request,
414
+ f"🔄 Started provider sync for {len(currency_codes)} currencies"
415
+ )
416
+
417
+ except Exception as e:
418
+ messages.error(request, f"❌ Failed to sync providers: {str(e)}")
419
+
420
+ # ===== HELPER METHODS =====
421
+
422
+ def _get_current_stats(self) -> dict:
423
+ """Get current system statistics."""
424
+ try:
425
+ return {
426
+ 'total_currencies': Currency.objects.count(),
427
+ 'fiat_count': Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count(),
428
+ 'crypto_count': Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count(),
429
+ 'total_provider_currencies': ProviderCurrency.objects.count(),
430
+ 'enabled_provider_currencies': ProviderCurrency.objects.filter(is_enabled=True).count(),
431
+ 'top_currencies': Currency.objects.annotate(
432
+ provider_count=Count('provider_configs')
433
+ ).filter(provider_count__gt=0).order_by('-provider_count')[:3]
434
+ }
435
+ except Exception as e:
436
+ logger.warning(f"Failed to get current stats: {e}")
437
+ return {}
438
+
439
+ def _generate_update_message(self, stats: dict) -> str:
440
+ """Generate HTML message for update status."""
441
+ top_currencies_html = ""
442
+ if 'top_currencies' in stats:
443
+ for currency in stats['top_currencies']:
444
+ provider_count = getattr(currency, 'provider_count', 0)
445
+ top_currencies_html += f'<li><strong>{currency.code}:</strong> {provider_count} providers</li>'
446
+
447
+ return f'''
448
+ <div class="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg border-l-4 border-green-500">
449
+ <h3 class="text-lg font-semibold text-green-800 dark:text-green-200 mb-3">🚀 Universal Update Started</h3>
450
+
451
+ <div class="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-lg mb-3 border border-yellow-200 dark:border-yellow-700">
452
+ <p class="text-yellow-800 dark:text-yellow-200 font-medium">⏳ Background tasks running:</p>
453
+ <ul class="text-sm text-yellow-700 dark:text-yellow-300 mt-2 space-y-1">
454
+ <li>1️⃣ Populating missing currencies...</li>
455
+ <li>2️⃣ Syncing provider data...</li>
456
+ <li>3️⃣ Updating USD exchange rates...</li>
457
+ </ul>
458
+ <p class="text-xs text-yellow-600 dark:text-yellow-400 mt-2">💡 Refresh page in 2-3 minutes to see results</p>
459
+ </div>
460
+
461
+ <div class="grid grid-cols-3 gap-3 mb-3">
462
+ <div class="bg-white dark:bg-gray-800 p-3 rounded-lg border">
463
+ <span class="text-sm text-gray-600 dark:text-gray-400">Total Currencies</span>
464
+ <p class="text-xl font-bold text-gray-900 dark:text-gray-100">{stats.get('total_currencies', 0)}</p>
465
+ </div>
466
+ <div class="bg-white dark:bg-gray-800 p-3 rounded-lg border">
467
+ <span class="text-sm text-gray-600 dark:text-gray-400">Fiat / Crypto</span>
468
+ <p class="text-xl font-bold">
469
+ <span class="text-blue-600">{stats.get('fiat_count', 0)}</span> /
470
+ <span class="text-orange-600">{stats.get('crypto_count', 0)}</span>
471
+ </p>
472
+ </div>
473
+ <div class="bg-white dark:bg-gray-800 p-3 rounded-lg border">
474
+ <span class="text-sm text-gray-600 dark:text-gray-400">Provider Mappings</span>
475
+ <p class="text-xl font-bold text-green-600">{stats.get('enabled_provider_currencies', 0)}</p>
476
+ </div>
477
+ </div>
478
+
479
+ {f'<div class="bg-white dark:bg-gray-800 p-3 rounded-lg border"><h4 class="font-semibold mb-2">🚀 Top Currencies</h4><ul class="text-sm space-y-1">{top_currencies_html}</ul></div>' if top_currencies_html else ''}
480
+ </div>
481
+ '''
482
+
483
+
484
+ @admin.register(Network)
485
+ class NetworkAdmin(ModelAdmin):
486
+ """Admin interface for blockchain networks."""
487
+
488
+ list_display = [
489
+ 'code_display',
490
+ 'name_display',
491
+ 'currency_count_badge',
492
+ 'created_at_display'
493
+ ]
494
+
495
+ search_fields = ['code', 'name']
496
+
497
+ readonly_fields = ['created_at', 'updated_at']
498
+
499
+ @display(description="Code", ordering='code')
500
+ def code_display(self, obj):
501
+ """Display network code with styling."""
502
+ return format_html(
503
+ '<span class="font-mono font-bold text-purple-600 dark:text-purple-400">{}</span>',
504
+ obj.code
505
+ )
506
+
507
+ @display(description="Name", ordering='name')
508
+ def name_display(self, obj):
509
+ """Display network name."""
510
+ return obj.name
511
+
512
+ @display(description="Currencies")
513
+ def currency_count_badge(self, obj):
514
+ """Display currency count for this network."""
515
+ count = ProviderCurrency.objects.filter(network=obj).count()
516
+ if count > 0:
101
517
  return format_html(
102
- '<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Inactive</span>'
518
+ '<span class="inline-flex items-center rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-200">'
519
+ '{} currenc{}'
520
+ '</span>',
521
+ count,
522
+ 'ies' if count != 1 else 'y'
103
523
  )
524
+ return format_html(
525
+ '<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-900 dark:text-gray-200">'
526
+ 'No currencies'
527
+ '</span>'
528
+ )
104
529
 
105
- @display(description="Created")
530
+ @display(description="Created", ordering='created_at')
106
531
  def created_at_display(self, obj):
107
532
  """Display creation date."""
108
533
  return naturaltime(obj.created_at)
109
534
 
110
535
 
111
- @admin.register(CurrencyNetwork)
112
- class CurrencyNetworkAdmin(ModelAdmin):
113
- """Admin interface for currency networks."""
536
+ @admin.register(ProviderCurrency)
537
+ class ProviderCurrencyAdmin(ModelAdmin):
538
+ """Admin interface for provider-specific currency configurations."""
114
539
 
115
540
  list_display = [
541
+ 'provider_currency_code_display',
542
+ 'provider_name_badge',
543
+ 'base_currency_display',
116
544
  'network_display',
117
- 'currency_display',
118
- 'status_display',
119
- 'confirmations_display',
120
- 'created_at_display'
545
+ 'usd_value_display',
546
+ 'status_badges',
547
+ 'updated_at_display'
121
548
  ]
122
549
 
123
- list_display_links = ['network_display']
550
+ list_filter = [
551
+ 'provider',
552
+ 'is_enabled',
553
+ 'currency__currency_type',
554
+ 'network'
555
+ ]
124
556
 
125
- search_fields = ['network_name', 'network_code', 'currency__code', 'currency__name']
557
+ search_fields = [
558
+ 'provider_currency_code',
559
+ 'currency__code',
560
+ 'currency__name',
561
+ 'network__code'
562
+ ]
126
563
 
127
- list_filter = ['currency', 'is_active', 'created_at']
564
+ readonly_fields = [
565
+ 'created_at',
566
+ 'updated_at'
567
+ ]
128
568
 
129
- readonly_fields = ['created_at', 'updated_at']
569
+ def get_queryset(self, request):
570
+ """Optimize queryset with related objects."""
571
+ return super().get_queryset(request).select_related(
572
+ 'currency', 'network'
573
+ )
130
574
 
131
- fieldsets = [
132
- ('Network Information', {
133
- 'fields': ['currency', 'network_name', 'network_code']
134
- }),
135
- ('Configuration', {
136
- 'fields': ['confirmation_blocks', 'is_active']
137
- }),
138
- ('Timestamps', {
139
- 'fields': ['created_at', 'updated_at'],
140
- 'classes': ['collapse']
141
- })
142
- ]
575
+ @display(description="Provider Code", ordering='provider_currency_code')
576
+ def provider_currency_code_display(self, obj):
577
+ """Display provider-specific currency code."""
578
+ return format_html(
579
+ '<span class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">{}</span>',
580
+ obj.provider_currency_code
581
+ )
143
582
 
144
- @display(description="Network")
145
- def network_display(self, obj):
146
- """Display network information."""
583
+ @display(description="Provider", ordering='provider')
584
+ def provider_name_badge(self, obj):
585
+ """Display provider name with badge."""
586
+ color_map = {
587
+ 'nowpayments': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
588
+ 'cryptomus': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
589
+ 'cryptapi': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
590
+ }
591
+
592
+ color_class = color_map.get(obj.provider.lower(), 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200')
593
+
147
594
  return format_html(
148
- '<strong>{}</strong><br><small>{}</small>',
149
- obj.network_name,
150
- obj.network_code
595
+ '<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {}">{}</span>',
596
+ color_class,
597
+ obj.provider.title()
151
598
  )
152
599
 
153
- @display(description="Currency")
154
- def currency_display(self, obj):
155
- """Display currency information."""
600
+ @display(description="Currency", ordering='currency__code')
601
+ def base_currency_display(self, obj):
602
+ """Display base currency with type indicator."""
603
+ type_icon = "💰" if obj.currency.currency_type == Currency.CurrencyType.FIAT else "₿"
156
604
  return format_html(
157
- '<strong>{}</strong> {}<br><small>{}</small>',
158
- obj.currency.code,
159
- obj.currency.symbol,
160
- obj.currency.name
605
+ '{} <span class="font-bold">{}</span>',
606
+ type_icon,
607
+ obj.currency.code
161
608
  )
162
609
 
163
- @display(description="Status")
164
- def status_display(self, obj):
165
- """Display status badge."""
166
- if obj.is_active:
610
+ @display(description="Network", ordering='network__code')
611
+ def network_display(self, obj):
612
+ """Display network information."""
613
+ if obj.network:
167
614
  return format_html(
168
- '<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Active</span>'
615
+ '<span class="text-purple-600 dark:text-purple-400">{}</span>',
616
+ obj.network.code
169
617
  )
170
- else:
618
+ return format_html('<span class="text-gray-500">—</span>')
619
+
620
+ @display(description="USD Value")
621
+ def usd_value_display(self, obj):
622
+ """Display USD value with proper formatting."""
623
+ try:
624
+ if not obj.usd_rate:
625
+ return format_html('<span class="text-gray-500">No rate</span>')
626
+
627
+ usd_rate = float(obj.usd_rate)
628
+
629
+ if obj.currency.currency_type == Currency.CurrencyType.FIAT:
630
+ # Fiat: show tokens per USD
631
+ tokens_per_usd = 1.0 / usd_rate if usd_rate > 0 else 0
632
+ return format_html(
633
+ '<span class="text-blue-600 dark:text-blue-400">$1 = {} {}</span>',
634
+ f"{tokens_per_usd:.4f}",
635
+ obj.currency.code
636
+ )
637
+ else:
638
+ # Crypto: show USD value
639
+ if usd_rate > 1000:
640
+ rate_display = f"${usd_rate:,.0f}"
641
+ elif usd_rate > 1:
642
+ rate_display = f"${usd_rate:,.2f}"
643
+ elif usd_rate > 0.01:
644
+ rate_display = f"${usd_rate:.4f}"
645
+ else:
646
+ rate_display = f"${usd_rate:.8f}"
647
+
648
+ return format_html(
649
+ '<span class="text-green-600 dark:text-green-400">1 {} = {}</span>',
650
+ obj.currency.code,
651
+ rate_display
652
+ )
653
+
654
+ except Exception as e:
171
655
  return format_html(
172
- '<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Inactive</span>'
656
+ '<span class="text-red-500">Error: {}</span>',
657
+ str(e)[:15]
173
658
  )
174
659
 
175
- @display(description="Confirmations")
176
- def confirmations_display(self, obj):
177
- """Display confirmation blocks."""
178
- return format_html(
179
- '<span style="font-weight: bold;">{}</span> blocks',
180
- obj.confirmation_blocks
181
- )
660
+ @display(description="Status")
661
+ def status_badges(self, obj):
662
+ """Display status badges."""
663
+ badges = []
664
+
665
+ if obj.is_enabled:
666
+ 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">✅ Enabled</span>')
667
+ else:
668
+ 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">❌ Disabled</span>')
669
+
670
+ # Note: is_popular and is_stable fields don't exist in model
671
+ # These could be added later or calculated based on other criteria
672
+
673
+ return format_html(' '.join(badges))
182
674
 
183
- @display(description="Created")
184
- def created_at_display(self, obj):
185
- """Display creation date."""
186
- return naturaltime(obj.created_at)
675
+ @display(description="Updated", ordering='updated_at')
676
+ def updated_at_display(self, obj):
677
+ """Display last update time."""
678
+ return naturaltime(obj.updated_at)