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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -9
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +207 -67
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -284
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +75 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -498
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/managers/__init__.py +0 -22
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -55
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -297
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -62
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -111
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
9
|
-
from
|
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,
|
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
|
-
"""
|
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
|
-
'
|
21
|
-
'
|
22
|
-
'
|
23
|
-
'
|
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 = ['
|
57
|
+
list_display_links = ['code_display']
|
28
58
|
|
29
|
-
search_fields = [
|
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 = [
|
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': [
|
42
|
-
|
43
|
-
|
44
|
-
|
87
|
+
'fields': [
|
88
|
+
'code',
|
89
|
+
'name',
|
90
|
+
'currency_type',
|
91
|
+
'symbol',
|
92
|
+
'decimal_places'
|
93
|
+
]
|
45
94
|
}),
|
46
|
-
('
|
47
|
-
'fields': [
|
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
|
-
|
56
|
-
|
57
|
-
|
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
|
-
'<
|
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="
|
66
|
-
def
|
67
|
-
"""Display currency
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
if obj.
|
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
|
-
'<
|
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
|
-
|
89
|
-
|
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="
|
94
|
-
def
|
95
|
-
"""Display
|
96
|
-
|
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
|
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
|
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(
|
112
|
-
class
|
113
|
-
"""Admin interface for currency
|
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
|
-
'
|
118
|
-
'
|
119
|
-
'
|
120
|
-
'created_at_display'
|
545
|
+
'usd_value_display',
|
546
|
+
'status_badges',
|
547
|
+
'updated_at_display'
|
121
548
|
]
|
122
549
|
|
123
|
-
|
550
|
+
list_filter = [
|
551
|
+
'provider',
|
552
|
+
'is_enabled',
|
553
|
+
'currency__currency_type',
|
554
|
+
'network'
|
555
|
+
]
|
124
556
|
|
125
|
-
search_fields = [
|
557
|
+
search_fields = [
|
558
|
+
'provider_currency_code',
|
559
|
+
'currency__code',
|
560
|
+
'currency__name',
|
561
|
+
'network__code'
|
562
|
+
]
|
126
563
|
|
127
|
-
|
564
|
+
readonly_fields = [
|
565
|
+
'created_at',
|
566
|
+
'updated_at'
|
567
|
+
]
|
128
568
|
|
129
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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="
|
145
|
-
def
|
146
|
-
"""Display
|
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
|
-
'<
|
149
|
-
|
150
|
-
obj.
|
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
|
155
|
-
"""Display currency
|
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
|
-
'
|
158
|
-
|
159
|
-
obj.currency.
|
160
|
-
obj.currency.name
|
605
|
+
'{} <span class="font-bold">{}</span>',
|
606
|
+
type_icon,
|
607
|
+
obj.currency.code
|
161
608
|
)
|
162
609
|
|
163
|
-
@display(description="
|
164
|
-
def
|
165
|
-
"""Display
|
166
|
-
if obj.
|
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
|
615
|
+
'<span class="text-purple-600 dark:text-purple-400">{}</span>',
|
616
|
+
obj.network.code
|
169
617
|
)
|
170
|
-
|
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
|
656
|
+
'<span class="text-red-500">Error: {}</span>',
|
657
|
+
str(e)[:15]
|
173
658
|
)
|
174
659
|
|
175
|
-
@display(description="
|
176
|
-
def
|
177
|
-
"""Display
|
178
|
-
|
179
|
-
|
180
|
-
|
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="
|
184
|
-
def
|
185
|
-
"""Display
|
186
|
-
return naturaltime(obj.
|
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)
|