django-cfg 1.2.29__py3-none-any.whl → 1.2.31__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/payments/admin/__init__.py +3 -2
- django_cfg/apps/payments/admin/balance_admin.py +18 -18
- django_cfg/apps/payments/admin/currencies_admin.py +319 -131
- django_cfg/apps/payments/admin/payments_admin.py +15 -4
- django_cfg/apps/payments/config/module.py +2 -2
- django_cfg/apps/payments/config/utils.py +2 -2
- django_cfg/apps/payments/decorators.py +2 -2
- django_cfg/apps/payments/management/commands/README.md +95 -127
- django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
- django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
- django_cfg/apps/payments/managers/__init__.py +3 -2
- django_cfg/apps/payments/managers/balance_manager.py +2 -2
- django_cfg/apps/payments/managers/currency_manager.py +272 -49
- django_cfg/apps/payments/managers/payment_manager.py +161 -13
- django_cfg/apps/payments/middleware/api_access.py +2 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
- django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
- django_cfg/apps/payments/models/__init__.py +3 -2
- django_cfg/apps/payments/models/currencies.py +187 -71
- django_cfg/apps/payments/models/payments.py +3 -2
- django_cfg/apps/payments/serializers/__init__.py +3 -2
- django_cfg/apps/payments/serializers/currencies.py +20 -12
- django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
- django_cfg/apps/payments/services/core/balance_service.py +2 -2
- django_cfg/apps/payments/services/core/fallback_service.py +2 -2
- django_cfg/apps/payments/services/core/payment_service.py +3 -6
- django_cfg/apps/payments/services/core/subscription_service.py +4 -7
- django_cfg/apps/payments/services/internal_types.py +171 -7
- django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
- django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
- django_cfg/apps/payments/services/providers/base.py +144 -43
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
- django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
- django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
- django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
- django_cfg/apps/payments/services/providers/registry.py +294 -11
- django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
- django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
- django_cfg/apps/payments/services/security/error_handler.py +6 -8
- django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
- django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/payment_signals.py +11 -5
- django_cfg/apps/payments/signals/subscription_signals.py +2 -2
- django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
- django_cfg/apps/payments/templates/payments/base.html +4 -4
- django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
- django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
- django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
- django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
- django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
- django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
- django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
- django_cfg/apps/payments/templates/payments/stats.html +261 -0
- django_cfg/apps/payments/templates/payments/test.html +213 -0
- django_cfg/apps/payments/urls.py +3 -1
- django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -0
- django_cfg/apps/payments/utils/__init__.py +1 -3
- django_cfg/apps/payments/utils/billing_utils.py +2 -2
- django_cfg/apps/payments/utils/config_utils.py +2 -8
- django_cfg/apps/payments/utils/validation_utils.py +2 -2
- django_cfg/apps/payments/views/__init__.py +3 -2
- django_cfg/apps/payments/views/currency_views.py +31 -20
- django_cfg/apps/payments/views/payment_views.py +2 -2
- django_cfg/apps/payments/views/templates/ajax.py +141 -2
- django_cfg/apps/payments/views/templates/base.py +21 -13
- django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
- django_cfg/apps/payments/views/templates/payment_management.py +34 -40
- django_cfg/apps/payments/views/templates/stats.py +8 -4
- django_cfg/apps/payments/views/webhook_views.py +2 -2
- django_cfg/apps/payments/viewsets.py +3 -2
- 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/core/config.py +35 -0
- django_cfg/models/payments.py +2 -8
- 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_unfold/dashboard.py +7 -2
- 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-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
- 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/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
@@ -6,7 +6,7 @@ from .balance_admin import UserBalanceAdmin, TransactionAdmin
|
|
6
6
|
from .payments_admin import UniversalPaymentAdmin
|
7
7
|
from .subscriptions_admin import SubscriptionAdmin, EndpointGroupAdmin
|
8
8
|
from .api_keys_admin import APIKeyAdmin
|
9
|
-
from .currencies_admin import CurrencyAdmin,
|
9
|
+
from .currencies_admin import CurrencyAdmin, NetworkAdmin, ProviderCurrencyAdmin
|
10
10
|
from .tariffs_admin import TariffAdmin, TariffEndpointGroupAdmin
|
11
11
|
|
12
12
|
__all__ = [
|
@@ -17,7 +17,8 @@ __all__ = [
|
|
17
17
|
'EndpointGroupAdmin',
|
18
18
|
'APIKeyAdmin',
|
19
19
|
'CurrencyAdmin',
|
20
|
-
'
|
20
|
+
'NetworkAdmin',
|
21
|
+
'ProviderCurrencyAdmin',
|
21
22
|
'TariffAdmin',
|
22
23
|
'TariffEndpointGroupAdmin',
|
23
24
|
]
|
@@ -100,8 +100,8 @@ class UserBalanceAdmin(ModelAdmin):
|
|
100
100
|
color = '#dc3545' # Red
|
101
101
|
|
102
102
|
return format_html(
|
103
|
-
'<span style="color: {}; font-weight: bold;">${
|
104
|
-
color, amount
|
103
|
+
'<span style="color: {}; font-weight: bold;">${}</span>',
|
104
|
+
color, f"{float(amount):.2f}"
|
105
105
|
)
|
106
106
|
|
107
107
|
@display(description="Reserved")
|
@@ -109,8 +109,8 @@ class UserBalanceAdmin(ModelAdmin):
|
|
109
109
|
"""Display reserved amount."""
|
110
110
|
if obj.reserved_usd > 0:
|
111
111
|
return format_html(
|
112
|
-
'<span style="color: #6c757d;">${
|
113
|
-
obj.reserved_usd
|
112
|
+
'<span style="color: #6c757d;">${}</span>',
|
113
|
+
f"{float(obj.reserved_usd):.2f}"
|
114
114
|
)
|
115
115
|
return "—"
|
116
116
|
|
@@ -119,8 +119,8 @@ class UserBalanceAdmin(ModelAdmin):
|
|
119
119
|
"""Display available balance."""
|
120
120
|
available = obj.amount_usd - obj.reserved_usd
|
121
121
|
return format_html(
|
122
|
-
'<span style="font-weight: bold;">${
|
123
|
-
available
|
122
|
+
'<span style="font-weight: bold;">${}</span>',
|
123
|
+
f"{float(available):.2f}"
|
124
124
|
)
|
125
125
|
|
126
126
|
@display(description="Last Transaction")
|
@@ -129,10 +129,10 @@ class UserBalanceAdmin(ModelAdmin):
|
|
129
129
|
last_transaction = obj.user.transactions.order_by('-created_at').first()
|
130
130
|
if last_transaction:
|
131
131
|
return format_html(
|
132
|
-
'<span style="color: {};">{} ${
|
132
|
+
'<span style="color: {};">{} ${}</span><br><small>{}</small>',
|
133
133
|
'#28a745' if last_transaction.amount_usd > 0 else '#dc3545',
|
134
134
|
'+' if last_transaction.amount_usd > 0 else '',
|
135
|
-
abs(last_transaction.amount_usd),
|
135
|
+
f"{float(abs(last_transaction.amount_usd)):.2f}",
|
136
136
|
naturaltime(last_transaction.created_at)
|
137
137
|
)
|
138
138
|
return "No transactions"
|
@@ -152,18 +152,18 @@ class UserBalanceAdmin(ModelAdmin):
|
|
152
152
|
return format_html(
|
153
153
|
'<div style="line-height: 1.6;">'
|
154
154
|
'<strong>Statistics:</strong><br>'
|
155
|
-
'• Total Credited: <span style="color: #28a745;">${
|
156
|
-
'• Total Debited: <span style="color: #dc3545;">${
|
157
|
-
'• Net Balance: <span style="color: {};">${
|
155
|
+
'• Total Credited: <span style="color: #28a745;">${}</span><br>'
|
156
|
+
'• Total Debited: <span style="color: #dc3545;">${}</span><br>'
|
157
|
+
'• Net Balance: <span style="color: {};">${}</span><br>'
|
158
158
|
'• Total Transactions: {}<br>'
|
159
|
-
'• Available Balance: <strong>${
|
159
|
+
'• Available Balance: <strong>${}</strong>'
|
160
160
|
'</div>',
|
161
|
-
total_credited,
|
162
|
-
total_debited,
|
161
|
+
f"{float(total_credited):.2f}",
|
162
|
+
f"{float(total_debited):.2f}",
|
163
163
|
'#28a745' if (total_credited - total_debited) > 0 else '#dc3545',
|
164
|
-
total_credited - total_debited,
|
164
|
+
f"{float(total_credited - total_debited):.2f}",
|
165
165
|
transaction_count,
|
166
|
-
obj.amount_usd - obj.reserved_usd
|
166
|
+
f"{float(obj.amount_usd - obj.reserved_usd):.2f}"
|
167
167
|
)
|
168
168
|
|
169
169
|
balance_statistics.short_description = "Balance Statistics"
|
@@ -381,7 +381,7 @@ class TransactionAdmin(ModelAdmin):
|
|
381
381
|
'• ID: {}<br>'
|
382
382
|
'• User: {} ({})<br>'
|
383
383
|
'• Type: {}<br>'
|
384
|
-
'• Amount: <span style="color: {};">${
|
384
|
+
'• Amount: <span style="color: {};">${}</span><br>'
|
385
385
|
'• Description: {}<br>'
|
386
386
|
'• Created: {}<br>'
|
387
387
|
'{}'
|
@@ -392,7 +392,7 @@ class TransactionAdmin(ModelAdmin):
|
|
392
392
|
obj.user.email,
|
393
393
|
obj.get_transaction_type_display(),
|
394
394
|
'#28a745' if obj.amount_usd > 0 else '#dc3545',
|
395
|
-
obj.amount_usd,
|
395
|
+
f"{float(obj.amount_usd):.2f}",
|
396
396
|
obj.description,
|
397
397
|
obj.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
398
398
|
f'• Payment: {obj.payment.internal_payment_id}<br>' if obj.payment else '',
|
@@ -5,46 +5,54 @@ Admin interface for currencies.
|
|
5
5
|
from django.contrib import admin
|
6
6
|
from django.utils.html import format_html
|
7
7
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
8
|
+
from django.contrib import messages
|
9
|
+
from django.shortcuts import redirect
|
10
|
+
from django.core.management import call_command
|
11
|
+
from django.utils.safestring import mark_safe
|
8
12
|
from unfold.admin import ModelAdmin
|
9
|
-
from unfold.decorators import display
|
13
|
+
from unfold.decorators import display, action
|
14
|
+
from unfold.enums import ActionVariant
|
15
|
+
from unfold.admin import TabularInline
|
10
16
|
|
11
|
-
from ..models import Currency,
|
17
|
+
from ..models import Currency, Network, ProviderCurrency
|
12
18
|
from .filters import CurrencyTypeFilter
|
13
19
|
|
14
20
|
|
15
21
|
@admin.register(Currency)
|
16
22
|
class CurrencyAdmin(ModelAdmin):
|
17
|
-
"""Admin interface for currencies."""
|
23
|
+
"""Admin interface for clean base currencies."""
|
24
|
+
|
25
|
+
# Custom template to show statistics above listing
|
26
|
+
change_list_template = 'admin/payments/currency/change_list.html'
|
18
27
|
|
19
28
|
list_display = [
|
20
|
-
'
|
21
|
-
'
|
22
|
-
'
|
23
|
-
'
|
24
|
-
'
|
29
|
+
'code',
|
30
|
+
'name',
|
31
|
+
'currency_type',
|
32
|
+
'usd_rate_display',
|
33
|
+
'provider_count',
|
34
|
+
'created_at'
|
25
35
|
]
|
26
36
|
|
27
|
-
list_display_links = ['
|
37
|
+
list_display_links = ['code']
|
28
38
|
|
29
|
-
search_fields = ['code', 'name'
|
39
|
+
search_fields = ['code', 'name']
|
30
40
|
|
31
41
|
list_filter = [
|
32
|
-
|
33
|
-
'is_active',
|
42
|
+
'currency_type',
|
34
43
|
'created_at'
|
35
44
|
]
|
36
45
|
|
37
|
-
readonly_fields = ['
|
46
|
+
readonly_fields = ['created_at', 'updated_at']
|
47
|
+
|
48
|
+
# Unfold action buttons above listing - only one universal button!
|
49
|
+
actions_list = [
|
50
|
+
'universal_update_all'
|
51
|
+
]
|
38
52
|
|
39
53
|
fieldsets = [
|
40
54
|
('Currency Information', {
|
41
|
-
'fields': ['code', 'name', '
|
42
|
-
}),
|
43
|
-
('Configuration', {
|
44
|
-
'fields': ['decimal_places', 'min_payment_amount', 'is_active']
|
45
|
-
}),
|
46
|
-
('Exchange Rate', {
|
47
|
-
'fields': ['usd_rate', 'rate_updated_at']
|
55
|
+
'fields': ['code', 'name', 'currency_type']
|
48
56
|
}),
|
49
57
|
('Timestamps', {
|
50
58
|
'fields': ['created_at', 'updated_at'],
|
@@ -52,135 +60,315 @@ class CurrencyAdmin(ModelAdmin):
|
|
52
60
|
})
|
53
61
|
]
|
54
62
|
|
55
|
-
@display(description="
|
56
|
-
def
|
57
|
-
"""
|
63
|
+
@display(description="USD Rate", ordering='usd_rate')
|
64
|
+
def usd_rate_display(self, obj):
|
65
|
+
"""Show USD exchange rate with cache status."""
|
66
|
+
if obj.usd_rate and obj.rate_updated_at:
|
67
|
+
# Check if rate is fresh (less than 24 hours)
|
68
|
+
from django.utils import timezone
|
69
|
+
from datetime import timedelta
|
70
|
+
|
71
|
+
is_fresh = timezone.now() - obj.rate_updated_at < timedelta(hours=24)
|
72
|
+
color_class = "text-green-600 dark:text-green-400" if is_fresh else "text-orange-600 dark:text-orange-400"
|
73
|
+
icon = "🟢" if is_fresh else "🟠"
|
74
|
+
|
75
|
+
if obj.currency_type == 'fiat':
|
76
|
+
# Fiat currencies show as 1 USD = X CURRENCY
|
77
|
+
tokens_per_usd = 1.0 / float(obj.usd_rate) if obj.usd_rate > 0 else 0
|
78
|
+
return format_html(
|
79
|
+
'<span class="{}">{} $1 = {} {}</span><br><small class="text-xs text-gray-500">Updated: {}</small>',
|
80
|
+
color_class,
|
81
|
+
icon,
|
82
|
+
f"{tokens_per_usd:.4f}",
|
83
|
+
obj.code,
|
84
|
+
naturaltime(obj.rate_updated_at)
|
85
|
+
)
|
86
|
+
else:
|
87
|
+
# Crypto currencies show as 1 CURRENCY = X USD
|
88
|
+
return format_html(
|
89
|
+
'<span class="{}">{} 1 {} = ${}</span><br><small class="text-xs text-gray-500">Updated: {}</small>',
|
90
|
+
color_class,
|
91
|
+
icon,
|
92
|
+
obj.code,
|
93
|
+
f"{float(obj.usd_rate):.8f}",
|
94
|
+
naturaltime(obj.rate_updated_at)
|
95
|
+
)
|
96
|
+
else:
|
97
|
+
return format_html(
|
98
|
+
'<span class="text-gray-500">❌ No rate</span><br><small class="text-xs text-gray-400">Never updated</small>'
|
99
|
+
)
|
100
|
+
|
101
|
+
@display(description="Providers")
|
102
|
+
def provider_count(self, obj):
|
103
|
+
"""Show how many providers support this currency."""
|
104
|
+
count = getattr(obj, 'provider_mappings', obj.provider_currency_set if hasattr(obj, 'provider_currency_set') else []).count()
|
105
|
+
if count > 0:
|
106
|
+
return format_html(
|
107
|
+
'<span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">{} providers</span>',
|
108
|
+
count
|
109
|
+
)
|
58
110
|
return format_html(
|
59
|
-
'<
|
60
|
-
obj.code,
|
61
|
-
obj.symbol,
|
62
|
-
obj.name
|
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">No providers</span>'
|
63
112
|
)
|
64
113
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
type_colors = {
|
69
|
-
'fiat': '#28a745',
|
70
|
-
'crypto': '#fd7e14',
|
71
|
-
}
|
114
|
+
def changelist_view(self, request, extra_context=None):
|
115
|
+
"""Override changelist view to add default statistics."""
|
116
|
+
extra_context = extra_context or {}
|
72
117
|
|
73
|
-
|
118
|
+
try:
|
119
|
+
from django.db.models import Count
|
120
|
+
|
121
|
+
# Get statistics for template
|
122
|
+
total_currencies = Currency.objects.count()
|
123
|
+
fiat_count = Currency.objects.filter(currency_type='fiat').count()
|
124
|
+
crypto_count = Currency.objects.filter(currency_type='crypto').count()
|
125
|
+
|
126
|
+
# Count provider mappings
|
127
|
+
total_provider_currencies = ProviderCurrency.objects.count()
|
128
|
+
enabled_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
|
129
|
+
|
130
|
+
# Count currencies with USD rates
|
131
|
+
currencies_with_rates = Currency.objects.filter(usd_rate__isnull=False).count()
|
132
|
+
rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
|
133
|
+
|
134
|
+
# Top popular currencies by provider count
|
135
|
+
top_currencies = Currency.objects.annotate(
|
136
|
+
provider_count=Count('provider_mappings')
|
137
|
+
).filter(provider_count__gt=0).order_by('-provider_count')[:5]
|
138
|
+
|
139
|
+
# Pass data to template
|
140
|
+
extra_context.update({
|
141
|
+
'total_currencies': total_currencies,
|
142
|
+
'fiat_count': fiat_count,
|
143
|
+
'crypto_count': crypto_count,
|
144
|
+
'total_provider_currencies': total_provider_currencies,
|
145
|
+
'enabled_provider_currencies': enabled_provider_currencies,
|
146
|
+
'currencies_with_rates': currencies_with_rates,
|
147
|
+
'rate_coverage': rate_coverage,
|
148
|
+
'top_currencies': top_currencies,
|
149
|
+
})
|
150
|
+
|
151
|
+
except Exception as e:
|
152
|
+
# If stats fail, just log and continue
|
153
|
+
import logging
|
154
|
+
logger = logging.getLogger(__name__)
|
155
|
+
logger.warning(f"Failed to generate currency stats: {e}")
|
74
156
|
|
75
|
-
return
|
76
|
-
'<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>',
|
77
|
-
color,
|
78
|
-
obj.get_currency_type_display()
|
79
|
-
)
|
157
|
+
return super().changelist_view(request, extra_context)
|
80
158
|
|
81
|
-
@display(description="USD Rate")
|
82
|
-
def rate_display(self, obj):
|
83
|
-
"""Display exchange rate."""
|
84
|
-
if obj.usd_rate != 1.0:
|
85
|
-
return format_html(
|
86
|
-
'<strong>1 {} = ${:.6f}</strong><br><small>Updated: {}</small>',
|
87
|
-
obj.code,
|
88
|
-
obj.usd_rate,
|
89
|
-
naturaltime(obj.rate_updated_at) if obj.rate_updated_at else 'Never'
|
90
|
-
)
|
91
|
-
return format_html('<span style="color: #6c757d;">Base currency</span>')
|
92
159
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
160
|
+
# Universal Admin Action - ONE BUTTON TO RULE THEM ALL!
|
161
|
+
|
162
|
+
@action(
|
163
|
+
description="🚀 Universal Update",
|
164
|
+
icon="sync",
|
165
|
+
variant=ActionVariant.SUCCESS,
|
166
|
+
url_path="universal-update"
|
167
|
+
)
|
168
|
+
def universal_update_all(self, request):
|
169
|
+
"""Universal update: populate missing currencies + sync providers + update rates + show stats."""
|
170
|
+
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
|
+
def background_update():
|
177
|
+
"""Background task for full update."""
|
178
|
+
try:
|
179
|
+
# 1. Populate missing currencies (fast, skip if exists)
|
180
|
+
call_command('manage_currencies', '--populate', '--skip-existing')
|
181
|
+
sleep(1)
|
182
|
+
|
183
|
+
# 2. Sync all providers (medium)
|
184
|
+
call_command('manage_providers', '--all')
|
185
|
+
sleep(1)
|
186
|
+
|
187
|
+
# 3. Update USD rates for all currencies (slower)
|
188
|
+
call_command('manage_currencies', '--rates-only')
|
189
|
+
|
190
|
+
except Exception as e:
|
191
|
+
print(f"Background universal update error: {e}")
|
192
|
+
|
193
|
+
# Start background update
|
194
|
+
thread = threading.Thread(target=background_update)
|
195
|
+
thread.daemon = True
|
196
|
+
thread.start()
|
197
|
+
|
198
|
+
# Show immediate stats while update is running
|
199
|
+
total_currencies = Currency.objects.count()
|
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()
|
204
|
+
|
205
|
+
# Top popular currencies by provider count
|
206
|
+
top_currencies = Currency.objects.annotate(
|
207
|
+
provider_count=Count('provider_mappings')
|
208
|
+
).filter(provider_count__gt=0).order_by('-provider_count')[:5]
|
209
|
+
|
210
|
+
currency_list = ""
|
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>'
|
213
|
+
|
214
|
+
stats_and_status_html = f'''
|
215
|
+
<div class="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-5 rounded-default border-l-4 border-green-500 mt-3">
|
216
|
+
<h3 class="text-lg font-semibold text-font-important-light dark:text-font-important-dark mb-4">🚀 Universal Update Started</h3>
|
217
|
+
|
218
|
+
<div class="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-default mb-4 border border-yellow-200 dark:border-yellow-700">
|
219
|
+
<p class="text-yellow-800 dark:text-yellow-200 font-medium">⏳ Background tasks running:</p>
|
220
|
+
<ul class="text-sm text-yellow-700 dark:text-yellow-300 mt-2 space-y-1">
|
221
|
+
<li>1️⃣ Populating missing currencies...</li>
|
222
|
+
<li>2️⃣ Syncing provider data...</li>
|
223
|
+
<li>3️⃣ Updating USD exchange rates...</li>
|
224
|
+
</ul>
|
225
|
+
<p class="text-xs text-yellow-600 dark:text-yellow-400 mt-2">💡 Refresh page in 2-3 minutes to see results</p>
|
226
|
+
</div>
|
227
|
+
|
228
|
+
<h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-3">📊 Current Statistics</h4>
|
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
|
+
'''
|
264
|
+
|
265
|
+
messages.success(request, mark_safe(stats_and_status_html))
|
266
|
+
|
267
|
+
except Exception as e:
|
268
|
+
messages.error(
|
269
|
+
request,
|
270
|
+
f"❌ Failed to start universal update: {str(e)}"
|
103
271
|
)
|
272
|
+
|
273
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/django_cfg_payments/currency/'))
|
274
|
+
|
275
|
+
|
276
|
+
|
277
|
+
# ===== NEW ADMIN CLASSES FOR NEW ARCHITECTURE =====
|
278
|
+
|
279
|
+
|
280
|
+
|
281
|
+
@admin.register(Network)
|
282
|
+
class NetworkAdmin(ModelAdmin):
|
283
|
+
"""Admin for blockchain networks."""
|
104
284
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
285
|
+
list_display = ["code", "name", "currency_count", "created_at"]
|
286
|
+
search_fields = ["code", "name"]
|
287
|
+
|
288
|
+
@display(description="Currencies")
|
289
|
+
def currency_count(self, obj):
|
290
|
+
"""Show currency count."""
|
291
|
+
count = ProviderCurrency.objects.filter(network=obj).count()
|
292
|
+
return f"{count} currencies"
|
109
293
|
|
110
294
|
|
111
|
-
@admin.register(
|
112
|
-
class
|
113
|
-
"""Admin
|
295
|
+
@admin.register(ProviderCurrency)
|
296
|
+
class ProviderCurrencyAdmin(ModelAdmin):
|
297
|
+
"""Admin for provider currencies."""
|
114
298
|
|
115
299
|
list_display = [
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
300
|
+
"provider_currency_code",
|
301
|
+
"provider_name",
|
302
|
+
"base_currency",
|
303
|
+
"network",
|
304
|
+
"usd_value_display",
|
305
|
+
"status_badges"
|
121
306
|
]
|
122
307
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
readonly_fields = ['created_at', 'updated_at']
|
130
|
-
|
131
|
-
fieldsets = [
|
132
|
-
('Network Information', {
|
133
|
-
'fields': ['currency', 'network_name', 'network_code']
|
134
|
-
}),
|
135
|
-
('Configuration', {
|
136
|
-
'fields': ['confirmation_blocks', 'is_active']
|
137
|
-
}),
|
138
|
-
('Timestamps', {
|
139
|
-
'fields': ['created_at', 'updated_at'],
|
140
|
-
'classes': ['collapse']
|
141
|
-
})
|
308
|
+
list_filter = [
|
309
|
+
"provider_name",
|
310
|
+
"is_enabled",
|
311
|
+
"is_popular",
|
312
|
+
"is_stable"
|
142
313
|
]
|
143
314
|
|
144
|
-
|
145
|
-
|
146
|
-
""
|
147
|
-
|
148
|
-
'<strong>{}</strong><br><small>{}</small>',
|
149
|
-
obj.network_name,
|
150
|
-
obj.network_code
|
151
|
-
)
|
152
|
-
|
153
|
-
@display(description="Currency")
|
154
|
-
def currency_display(self, obj):
|
155
|
-
"""Display currency information."""
|
156
|
-
return format_html(
|
157
|
-
'<strong>{}</strong> {}<br><small>{}</small>',
|
158
|
-
obj.currency.code,
|
159
|
-
obj.currency.symbol,
|
160
|
-
obj.currency.name
|
161
|
-
)
|
315
|
+
search_fields = [
|
316
|
+
"provider_currency_code",
|
317
|
+
"base_currency__code"
|
318
|
+
]
|
162
319
|
|
163
|
-
@display(description="
|
164
|
-
def
|
165
|
-
"""
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
320
|
+
@display(description="USD Value")
|
321
|
+
def usd_value_display(self, obj):
|
322
|
+
"""Show USD value for this provider currency."""
|
323
|
+
try:
|
324
|
+
usd_rate = obj.usd_rate
|
325
|
+
tokens_per_usd = obj.tokens_per_usd
|
326
|
+
|
327
|
+
if obj.base_currency.currency_type == 'fiat':
|
328
|
+
# Fiat: show how many tokens for $1
|
329
|
+
return format_html(
|
330
|
+
'<span class="text-blue-600 dark:text-blue-400">$1 = {} {}</span>',
|
331
|
+
f"{tokens_per_usd:.4f}",
|
332
|
+
obj.base_currency.code
|
333
|
+
)
|
334
|
+
else:
|
335
|
+
# Crypto: show USD value
|
336
|
+
if usd_rate > 1:
|
337
|
+
# High value crypto (like BTC)
|
338
|
+
return format_html(
|
339
|
+
'<span class="text-green-600 dark:text-green-400 font-semibold">1 {} = ${}</span>',
|
340
|
+
obj.base_currency.code,
|
341
|
+
f"{usd_rate:,.2f}"
|
342
|
+
)
|
343
|
+
elif usd_rate > 0.01:
|
344
|
+
# Medium value crypto
|
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
|
+
)
|
350
|
+
else:
|
351
|
+
# Low value crypto (show more decimals)
|
352
|
+
return format_html(
|
353
|
+
'<span class="text-green-600 dark:text-green-400">1 {} = ${}</span>',
|
354
|
+
obj.base_currency.code,
|
355
|
+
f"{usd_rate:.8f}"
|
356
|
+
)
|
357
|
+
except Exception as e:
|
171
358
|
return format_html(
|
172
|
-
'<span
|
359
|
+
'<span class="text-red-500">Error: {}</span>',
|
360
|
+
str(e)[:20]
|
173
361
|
)
|
174
362
|
|
175
|
-
@display(description="
|
176
|
-
def
|
177
|
-
"""Display
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
""
|
186
|
-
|
363
|
+
@display(description="Status")
|
364
|
+
def status_badges(self, obj):
|
365
|
+
"""Display status badges."""
|
366
|
+
badges = []
|
367
|
+
if obj.is_enabled:
|
368
|
+
badges.append("✅ Enabled")
|
369
|
+
if obj.is_popular:
|
370
|
+
badges.append("⭐ Popular")
|
371
|
+
if obj.is_stable:
|
372
|
+
badges.append("🔒 Stable")
|
373
|
+
return " | ".join(badges) if badges else "❌ Disabled"
|
374
|
+
|