django-cfg 1.2.31__py3-none-any.whl → 1.3.3__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 -10
- 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 +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- 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/cleanup_expired_data.py +419 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +297 -225
- django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
- django_cfg/apps/payments/management/commands/test_providers.py +434 -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 +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +153 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +172 -148
- 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 -285
- 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_service/__init__.py +143 -0
- django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
- django_cfg/apps/payments/services/{cache/base.py → cache_service/interfaces.py} +3 -1
- django_cfg/apps/payments/services/cache_service/keys.py +49 -0
- django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
- django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
- 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 +371 -465
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- 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 +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- 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 +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- 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 +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- 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/config.py +1 -1
- django_cfg/core/config.py +40 -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 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +227 -570
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/RECORD +162 -185
- 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 -146
- django_cfg/apps/payments/managers/__init__.py +0 -23
- 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 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- 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 -63
- 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/simple_cache.py +0 -135
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -461
- 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 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- 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/admin/payments/currency/change_list.html +0 -50
- 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 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- 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 -63
- 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 -122
- 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 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- 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 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- 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 -66
- django_cfg/core/integration.py +0 -160
- 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.31.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,7 @@
|
|
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
|
@@ -9,50 +11,92 @@ from django.contrib import messages
|
|
9
11
|
from django.shortcuts import redirect
|
10
12
|
from django.core.management import call_command
|
11
13
|
from django.utils.safestring import mark_safe
|
12
|
-
from
|
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
|
13
21
|
from unfold.decorators import display, action
|
14
22
|
from unfold.enums import ActionVariant
|
15
|
-
from unfold.admin import TabularInline
|
16
23
|
|
17
24
|
from ..models import Currency, Network, ProviderCurrency
|
18
|
-
from .filters import CurrencyTypeFilter
|
25
|
+
from .filters import CurrencyTypeFilter, CurrencyRateStatusFilter
|
26
|
+
from django_cfg.modules.django_logger import get_logger
|
27
|
+
|
28
|
+
logger = get_logger("currencies_admin")
|
19
29
|
|
20
30
|
|
21
31
|
@admin.register(Currency)
|
22
32
|
class CurrencyAdmin(ModelAdmin):
|
23
|
-
"""
|
33
|
+
"""
|
34
|
+
Modern Currency admin with Unfold styling and universal update functionality.
|
24
35
|
|
25
|
-
|
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
|
26
45
|
change_list_template = 'admin/payments/currency/change_list.html'
|
27
46
|
|
28
47
|
list_display = [
|
29
|
-
'
|
30
|
-
'
|
31
|
-
'
|
48
|
+
'code_display',
|
49
|
+
'name_display',
|
50
|
+
'currency_type_badge',
|
32
51
|
'usd_rate_display',
|
33
|
-
'
|
34
|
-
'
|
52
|
+
'provider_count_badge',
|
53
|
+
'rate_freshness',
|
54
|
+
'created_at_display'
|
35
55
|
]
|
36
56
|
|
37
|
-
list_display_links = ['
|
57
|
+
list_display_links = ['code_display']
|
38
58
|
|
39
|
-
search_fields = [
|
59
|
+
search_fields = [
|
60
|
+
'code',
|
61
|
+
'name',
|
62
|
+
'symbol'
|
63
|
+
]
|
40
64
|
|
41
65
|
list_filter = [
|
42
|
-
|
66
|
+
CurrencyTypeFilter,
|
67
|
+
CurrencyRateStatusFilter,
|
68
|
+
'is_active',
|
43
69
|
'created_at'
|
44
70
|
]
|
45
71
|
|
46
|
-
readonly_fields = [
|
72
|
+
readonly_fields = [
|
73
|
+
'created_at',
|
74
|
+
'updated_at',
|
75
|
+
'exchange_rate_source'
|
76
|
+
]
|
47
77
|
|
48
|
-
# Unfold
|
78
|
+
# Unfold actions
|
49
79
|
actions_list = [
|
50
|
-
'universal_update_all'
|
80
|
+
'universal_update_all',
|
81
|
+
'update_selected_rates',
|
82
|
+
'sync_provider_currencies'
|
51
83
|
]
|
52
84
|
|
53
85
|
fieldsets = [
|
54
86
|
('Currency Information', {
|
55
|
-
'fields': [
|
87
|
+
'fields': [
|
88
|
+
'code',
|
89
|
+
'name',
|
90
|
+
'currency_type',
|
91
|
+
'symbol',
|
92
|
+
'decimal_places'
|
93
|
+
]
|
94
|
+
}),
|
95
|
+
('Status & Configuration', {
|
96
|
+
'fields': [
|
97
|
+
'is_active',
|
98
|
+
'exchange_rate_source'
|
99
|
+
]
|
56
100
|
}),
|
57
101
|
('Timestamps', {
|
58
102
|
'fields': ['created_at', 'updated_at'],
|
@@ -60,315 +104,575 @@ class CurrencyAdmin(ModelAdmin):
|
|
60
104
|
})
|
61
105
|
]
|
62
106
|
|
63
|
-
|
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
|
+
)
|
123
|
+
return format_html(
|
124
|
+
'<span class="font-mono font-bold text-primary-600 dark:text-primary-400">{}</span>',
|
125
|
+
obj.code
|
126
|
+
)
|
127
|
+
|
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')
|
64
156
|
def usd_rate_display(self, obj):
|
65
|
-
"""
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
)
|
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()
|
162
|
+
|
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
|
+
)
|
167
|
+
|
168
|
+
# Check freshness (24 hours)
|
169
|
+
is_fresh = (
|
170
|
+
provider_currency.updated_at and
|
171
|
+
timezone.now() - provider_currency.updated_at < timedelta(hours=24)
|
172
|
+
)
|
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
|
180
|
+
return format_html(
|
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}",
|
186
|
+
obj.code,
|
187
|
+
naturaltime(provider_currency.updated_at) if provider_currency.updated_at else "Never"
|
188
|
+
)
|
96
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
|
+
|
97
199
|
return format_html(
|
98
|
-
'<
|
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"
|
99
207
|
)
|
100
208
|
|
101
209
|
@display(description="Providers")
|
102
|
-
def
|
103
|
-
"""
|
104
|
-
count = getattr(obj, '
|
210
|
+
def provider_count_badge(self, obj):
|
211
|
+
"""Display provider count with badge."""
|
212
|
+
count = getattr(obj, 'provider_count', 0)
|
105
213
|
if count > 0:
|
106
214
|
return format_html(
|
107
|
-
'<span class="inline-flex items-center rounded-full bg-
|
108
|
-
|
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 ''
|
109
220
|
)
|
110
221
|
return format_html(
|
111
|
-
'<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800">
|
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>'
|
112
225
|
)
|
113
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>')
|
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
|
+
|
114
253
|
def changelist_view(self, request, extra_context=None):
|
115
|
-
"""
|
254
|
+
"""Add statistics to changelist context."""
|
116
255
|
extra_context = extra_context or {}
|
117
256
|
|
118
257
|
try:
|
119
|
-
|
120
|
-
|
121
|
-
# Get statistics for template
|
258
|
+
# Basic statistics
|
122
259
|
total_currencies = Currency.objects.count()
|
123
|
-
fiat_count = Currency.objects.filter(currency_type=
|
124
|
-
crypto_count = Currency.objects.filter(currency_type=
|
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()
|
125
263
|
|
126
|
-
#
|
264
|
+
# Provider statistics
|
127
265
|
total_provider_currencies = ProviderCurrency.objects.count()
|
128
266
|
enabled_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
|
129
267
|
|
130
|
-
#
|
131
|
-
currencies_with_rates = Currency.objects.filter(
|
268
|
+
# Rate statistics
|
269
|
+
currencies_with_rates = Currency.objects.filter(
|
270
|
+
provider_configs__usd_rate__isnull=False
|
271
|
+
).distinct().count()
|
132
272
|
rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
|
133
273
|
|
134
|
-
#
|
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
|
135
281
|
top_currencies = Currency.objects.annotate(
|
136
|
-
provider_count=Count('
|
282
|
+
provider_count=Count('provider_configs')
|
137
283
|
).filter(provider_count__gt=0).order_by('-provider_count')[:5]
|
138
284
|
|
139
|
-
# Pass data to template
|
140
285
|
extra_context.update({
|
141
|
-
'
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
+
}
|
149
298
|
})
|
150
299
|
|
151
300
|
except Exception as e:
|
152
|
-
|
153
|
-
|
154
|
-
logger = logging.getLogger(__name__)
|
155
|
-
logger.warning(f"Failed to generate currency stats: {e}")
|
301
|
+
logger.warning(f"Failed to generate currency statistics: {e}")
|
302
|
+
extra_context['currency_stats'] = None
|
156
303
|
|
157
304
|
return super().changelist_view(request, extra_context)
|
158
305
|
|
159
|
-
|
160
|
-
# Universal Admin Action - ONE BUTTON TO RULE THEM ALL!
|
306
|
+
# ===== ADMIN ACTIONS =====
|
161
307
|
|
162
308
|
@action(
|
163
|
-
description="🚀 Universal Update",
|
309
|
+
description="🚀 Universal Update (All)",
|
164
310
|
icon="sync",
|
165
311
|
variant=ActionVariant.SUCCESS,
|
166
312
|
url_path="universal-update"
|
167
313
|
)
|
168
314
|
def universal_update_all(self, request):
|
169
|
-
"""
|
315
|
+
"""
|
316
|
+
Universal update: populate currencies + sync providers + update rates.
|
317
|
+
|
318
|
+
This is the main action that performs a complete system update.
|
319
|
+
"""
|
170
320
|
try:
|
171
|
-
import threading
|
172
|
-
from django.core.management import call_command
|
173
|
-
from django.db.models import Count
|
174
|
-
from time import sleep
|
175
|
-
|
176
321
|
def background_update():
|
177
|
-
"""Background task for
|
322
|
+
"""Background task for comprehensive update."""
|
178
323
|
try:
|
179
|
-
|
324
|
+
logger.info("Starting universal currency update")
|
325
|
+
|
326
|
+
# 1. Populate missing currencies (fast)
|
180
327
|
call_command('manage_currencies', '--populate', '--skip-existing')
|
181
|
-
sleep(1)
|
182
328
|
|
183
|
-
# 2. Sync all providers (medium)
|
329
|
+
# 2. Sync all providers (medium speed)
|
184
330
|
call_command('manage_providers', '--all')
|
185
|
-
sleep(1)
|
186
331
|
|
187
|
-
# 3. Update USD rates
|
332
|
+
# 3. Update USD rates (slower)
|
188
333
|
call_command('manage_currencies', '--rates-only')
|
189
334
|
|
335
|
+
logger.info("Universal currency update completed successfully")
|
336
|
+
|
190
337
|
except Exception as e:
|
191
|
-
|
338
|
+
logger.error(f"Universal update failed: {e}")
|
192
339
|
|
193
340
|
# Start background update
|
194
341
|
thread = threading.Thread(target=background_update)
|
195
342
|
thread.daemon = True
|
196
343
|
thread.start()
|
197
344
|
|
198
|
-
#
|
199
|
-
|
200
|
-
fiat_count = Currency.objects.filter(currency_type='fiat').count()
|
201
|
-
crypto_count = Currency.objects.filter(currency_type='crypto').count()
|
202
|
-
total_provider_currencies = ProviderCurrency.objects.count()
|
203
|
-
enabled_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
|
345
|
+
# Generate immediate statistics for user feedback
|
346
|
+
stats = self._get_current_stats()
|
204
347
|
|
205
|
-
|
206
|
-
|
207
|
-
provider_count=Count('provider_mappings')
|
208
|
-
).filter(provider_count__gt=0).order_by('-provider_count')[:5]
|
348
|
+
success_message = self._generate_update_message(stats)
|
349
|
+
messages.success(request, mark_safe(success_message))
|
209
350
|
|
210
|
-
|
211
|
-
for currency in top_currencies:
|
212
|
-
currency_list += f'<li class="text-font-default-light dark:text-font-default-dark"><span class="font-semibold text-primary-600 dark:text-primary-500">{currency.code}:</span> {currency.provider_count} providers</li>'
|
351
|
+
logger.info(f"Universal update initiated by user {request.user.username}")
|
213
352
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
<div class="grid grid-cols-2 gap-4 mb-4">
|
231
|
-
<div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
|
232
|
-
<span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Total currencies</span>
|
233
|
-
<p class="text-xl font-bold text-font-important-light dark:text-font-important-dark">{total_currencies}</p>
|
234
|
-
</div>
|
235
|
-
<div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
|
236
|
-
<span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Provider Mappings</span>
|
237
|
-
<p class="text-xl font-bold">
|
238
|
-
<span class="text-green-600 dark:text-green-400">{enabled_provider_currencies}</span>
|
239
|
-
<span class="text-font-subtle-light dark:text-font-subtle-dark mx-1">/</span>
|
240
|
-
<span class="text-gray-600 dark:text-gray-400">{total_provider_currencies}</span>
|
241
|
-
</p>
|
242
|
-
</div>
|
243
|
-
</div>
|
244
|
-
|
245
|
-
<div class="grid grid-cols-2 gap-4 mb-4">
|
246
|
-
<div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
|
247
|
-
<span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Fiat currencies</span>
|
248
|
-
<p class="text-xl font-bold text-blue-600 dark:text-blue-400">{fiat_count}</p>
|
249
|
-
</div>
|
250
|
-
<div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
|
251
|
-
<span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Cryptocurrencies</span>
|
252
|
-
<p class="text-xl font-bold text-orange-600 dark:text-orange-400">{crypto_count}</p>
|
253
|
-
</div>
|
254
|
-
</div>
|
255
|
-
|
256
|
-
<div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
|
257
|
-
<h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">🚀 Most Supported Currencies</h4>
|
258
|
-
<ul class="space-y-1 text-sm">
|
259
|
-
{currency_list}
|
260
|
-
</ul>
|
261
|
-
</div>
|
262
|
-
</div>
|
263
|
-
'''
|
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))
|
264
369
|
|
265
|
-
|
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
|
+
)
|
266
387
|
|
267
388
|
except Exception as e:
|
268
|
-
messages.error(
|
269
|
-
|
270
|
-
|
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"
|
271
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>'
|
272
446
|
|
273
|
-
return
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
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
|
+
'''
|
279
482
|
|
280
483
|
|
281
484
|
@admin.register(Network)
|
282
485
|
class NetworkAdmin(ModelAdmin):
|
283
|
-
"""Admin for blockchain networks."""
|
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']
|
284
496
|
|
285
|
-
|
286
|
-
|
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
|
287
511
|
|
288
512
|
@display(description="Currencies")
|
289
|
-
def
|
290
|
-
"""
|
513
|
+
def currency_count_badge(self, obj):
|
514
|
+
"""Display currency count for this network."""
|
291
515
|
count = ProviderCurrency.objects.filter(network=obj).count()
|
292
|
-
|
516
|
+
if count > 0:
|
517
|
+
return format_html(
|
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'
|
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
|
+
)
|
529
|
+
|
530
|
+
@display(description="Created", ordering='created_at')
|
531
|
+
def created_at_display(self, obj):
|
532
|
+
"""Display creation date."""
|
533
|
+
return naturaltime(obj.created_at)
|
293
534
|
|
294
535
|
|
295
536
|
@admin.register(ProviderCurrency)
|
296
537
|
class ProviderCurrencyAdmin(ModelAdmin):
|
297
|
-
"""Admin for provider
|
538
|
+
"""Admin interface for provider-specific currency configurations."""
|
298
539
|
|
299
540
|
list_display = [
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
541
|
+
'provider_currency_code_display',
|
542
|
+
'provider_name_badge',
|
543
|
+
'base_currency_display',
|
544
|
+
'network_display',
|
545
|
+
'usd_value_display',
|
546
|
+
'status_badges',
|
547
|
+
'updated_at_display'
|
306
548
|
]
|
307
549
|
|
308
550
|
list_filter = [
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
551
|
+
'provider',
|
552
|
+
'is_enabled',
|
553
|
+
'currency__currency_type',
|
554
|
+
'network'
|
313
555
|
]
|
314
556
|
|
315
557
|
search_fields = [
|
316
|
-
|
317
|
-
|
558
|
+
'provider_currency_code',
|
559
|
+
'currency__code',
|
560
|
+
'currency__name',
|
561
|
+
'network__code'
|
562
|
+
]
|
563
|
+
|
564
|
+
readonly_fields = [
|
565
|
+
'created_at',
|
566
|
+
'updated_at'
|
318
567
|
]
|
319
568
|
|
569
|
+
def get_queryset(self, request):
|
570
|
+
"""Optimize queryset with related objects."""
|
571
|
+
return super().get_queryset(request).select_related(
|
572
|
+
'currency', 'network'
|
573
|
+
)
|
574
|
+
|
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
|
+
)
|
582
|
+
|
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
|
+
|
594
|
+
return format_html(
|
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()
|
598
|
+
)
|
599
|
+
|
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 "₿"
|
604
|
+
return format_html(
|
605
|
+
'{} <span class="font-bold">{}</span>',
|
606
|
+
type_icon,
|
607
|
+
obj.currency.code
|
608
|
+
)
|
609
|
+
|
610
|
+
@display(description="Network", ordering='network__code')
|
611
|
+
def network_display(self, obj):
|
612
|
+
"""Display network information."""
|
613
|
+
if obj.network:
|
614
|
+
return format_html(
|
615
|
+
'<span class="text-purple-600 dark:text-purple-400">{}</span>',
|
616
|
+
obj.network.code
|
617
|
+
)
|
618
|
+
return format_html('<span class="text-gray-500">—</span>')
|
619
|
+
|
320
620
|
@display(description="USD Value")
|
321
621
|
def usd_value_display(self, obj):
|
322
|
-
"""
|
622
|
+
"""Display USD value with proper formatting."""
|
323
623
|
try:
|
324
|
-
|
325
|
-
|
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)
|
326
628
|
|
327
|
-
if obj.
|
328
|
-
# Fiat: show
|
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
|
329
632
|
return format_html(
|
330
633
|
'<span class="text-blue-600 dark:text-blue-400">$1 = {} {}</span>',
|
331
634
|
f"{tokens_per_usd:.4f}",
|
332
|
-
obj.
|
635
|
+
obj.currency.code
|
333
636
|
)
|
334
637
|
else:
|
335
638
|
# Crypto: show USD value
|
336
|
-
if usd_rate >
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
obj.base_currency.code,
|
341
|
-
f"{usd_rate:,.2f}"
|
342
|
-
)
|
639
|
+
if usd_rate > 1000:
|
640
|
+
rate_display = f"${usd_rate:,.0f}"
|
641
|
+
elif usd_rate > 1:
|
642
|
+
rate_display = f"${usd_rate:,.2f}"
|
343
643
|
elif usd_rate > 0.01:
|
344
|
-
|
345
|
-
return format_html(
|
346
|
-
'<span class="text-green-600 dark:text-green-400">1 {} = ${}</span>',
|
347
|
-
obj.base_currency.code,
|
348
|
-
f"{usd_rate:.4f}"
|
349
|
-
)
|
644
|
+
rate_display = f"${usd_rate:.4f}"
|
350
645
|
else:
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
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
|
+
|
357
654
|
except Exception as e:
|
358
655
|
return format_html(
|
359
656
|
'<span class="text-red-500">Error: {}</span>',
|
360
|
-
str(e)[:
|
657
|
+
str(e)[:15]
|
361
658
|
)
|
362
659
|
|
363
660
|
@display(description="Status")
|
364
661
|
def status_badges(self, obj):
|
365
662
|
"""Display status badges."""
|
366
663
|
badges = []
|
664
|
+
|
367
665
|
if obj.is_enabled:
|
368
|
-
badges.append("
|
369
|
-
|
370
|
-
badges.append("
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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))
|
674
|
+
|
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)
|