django-cfg 1.2.31__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 -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/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- 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 +150 -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/{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 +346 -467
- 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 +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -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/management/commands/currency_stats.py +0 -304
- 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/base.py +0 -30
- 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.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,162 +1,511 @@
|
|
1
1
|
"""
|
2
|
-
Admin
|
2
|
+
Subscription Admin interfaces with Unfold integration.
|
3
|
+
|
4
|
+
Advanced subscription lifecycle management and monitoring.
|
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.utils.safestring import mark_safe
|
13
|
+
from django.db.models import Count, Sum, Q
|
14
|
+
from django.utils import timezone
|
15
|
+
from datetime import timedelta
|
16
|
+
from typing import Optional
|
17
|
+
|
18
|
+
from unfold.admin import ModelAdmin, TabularInline
|
19
|
+
from unfold.decorators import display, action
|
20
|
+
from unfold.enums import ActionVariant
|
21
|
+
|
22
|
+
from ..models import Subscription, EndpointGroup, Tariff, TariffEndpointGroup
|
23
|
+
from .filters import SubscriptionTierFilter, SubscriptionStatusFilter, RecentActivityFilter
|
24
|
+
from django_cfg.modules.django_logger import get_logger
|
25
|
+
|
26
|
+
logger = get_logger("subscriptions_admin")
|
10
27
|
|
11
|
-
|
12
|
-
|
28
|
+
|
29
|
+
class TariffEndpointGroupInline(TabularInline):
|
30
|
+
"""Inline for tariff endpoint groups."""
|
31
|
+
model = TariffEndpointGroup
|
32
|
+
extra = 0
|
33
|
+
fields = ['endpoint_group', 'custom_rate_limit', 'is_enabled']
|
13
34
|
|
14
35
|
|
15
36
|
@admin.register(Subscription)
|
16
37
|
class SubscriptionAdmin(ModelAdmin):
|
17
|
-
"""
|
38
|
+
"""
|
39
|
+
Advanced Subscription admin with lifecycle management.
|
40
|
+
|
41
|
+
Features:
|
42
|
+
- Subscription lifecycle tracking
|
43
|
+
- Usage monitoring and alerts
|
44
|
+
- Bulk subscription operations
|
45
|
+
- Expiration management
|
46
|
+
- Tier-based filtering and actions
|
47
|
+
"""
|
48
|
+
|
49
|
+
# Custom template for subscription statistics
|
50
|
+
change_list_template = 'admin/payments/subscription/change_list.html'
|
18
51
|
|
19
52
|
list_display = [
|
20
53
|
'subscription_display',
|
21
54
|
'user_display',
|
22
|
-
'endpoint_group_display',
|
23
55
|
'tier_display',
|
24
56
|
'status_display',
|
25
57
|
'usage_display',
|
26
|
-
'
|
58
|
+
'expiry_display',
|
59
|
+
'created_at_display'
|
27
60
|
]
|
28
61
|
|
29
62
|
list_display_links = ['subscription_display']
|
30
63
|
|
31
64
|
search_fields = [
|
65
|
+
'id',
|
32
66
|
'user__email',
|
33
|
-
'
|
34
|
-
'
|
67
|
+
'user__username',
|
68
|
+
'tier'
|
35
69
|
]
|
36
70
|
|
37
71
|
list_filter = [
|
38
72
|
SubscriptionStatusFilter,
|
39
73
|
SubscriptionTierFilter,
|
40
|
-
|
41
|
-
|
42
|
-
'
|
43
|
-
'created_at'
|
74
|
+
RecentActivityFilter,
|
75
|
+
'created_at',
|
76
|
+
'expires_at'
|
44
77
|
]
|
45
78
|
|
46
79
|
readonly_fields = [
|
80
|
+
'id',
|
47
81
|
'created_at',
|
48
|
-
'updated_at'
|
82
|
+
'updated_at',
|
83
|
+
'last_request_at'
|
84
|
+
]
|
85
|
+
|
86
|
+
# Unfold actions
|
87
|
+
actions_list = [
|
88
|
+
'activate_subscriptions',
|
89
|
+
'suspend_subscriptions',
|
90
|
+
'extend_subscriptions',
|
49
91
|
]
|
50
92
|
|
51
93
|
fieldsets = [
|
52
94
|
('Subscription Information', {
|
53
|
-
'fields': [
|
95
|
+
'fields': [
|
96
|
+
'id',
|
97
|
+
'user',
|
98
|
+
'tier',
|
99
|
+
'status'
|
100
|
+
]
|
54
101
|
}),
|
55
|
-
('
|
56
|
-
'fields': [
|
102
|
+
('Usage & Limits', {
|
103
|
+
'fields': [
|
104
|
+
'total_requests',
|
105
|
+
'requests_per_hour',
|
106
|
+
'requests_per_day',
|
107
|
+
'last_request_at'
|
108
|
+
]
|
57
109
|
}),
|
58
|
-
('
|
59
|
-
'fields': [
|
110
|
+
('Billing & Expiry', {
|
111
|
+
'fields': [
|
112
|
+
'monthly_cost_usd',
|
113
|
+
'starts_at',
|
114
|
+
'expires_at',
|
115
|
+
'auto_renew'
|
116
|
+
]
|
60
117
|
}),
|
61
|
-
('
|
62
|
-
'fields': ['
|
118
|
+
('Timestamps', {
|
119
|
+
'fields': ['created_at', 'updated_at'],
|
63
120
|
'classes': ['collapse']
|
64
121
|
})
|
65
122
|
]
|
66
123
|
|
67
|
-
|
124
|
+
def get_queryset(self, request):
|
125
|
+
"""Optimize queryset with related data."""
|
126
|
+
return super().get_queryset(request).select_related('user').prefetch_related('endpoint_groups')
|
127
|
+
|
128
|
+
@display(description="Subscription", ordering='id')
|
68
129
|
def subscription_display(self, obj):
|
69
|
-
"""Display subscription
|
130
|
+
"""Display subscription ID with tier indicator."""
|
131
|
+
short_id = str(obj.id)[:8]
|
132
|
+
|
133
|
+
tier_icons = {
|
134
|
+
'free': '🆓',
|
135
|
+
'basic': '🥉',
|
136
|
+
'pro': '🥈',
|
137
|
+
'enterprise': '🥇'
|
138
|
+
}
|
139
|
+
|
140
|
+
tier_icon = tier_icons.get(obj.tier, '📋')
|
141
|
+
|
70
142
|
return format_html(
|
71
|
-
'<
|
72
|
-
|
73
|
-
|
143
|
+
'<div class="flex items-center space-x-2">'
|
144
|
+
'<span class="text-lg">{}</span>'
|
145
|
+
'<span class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded" title="Full ID: {}">{}</span>'
|
146
|
+
'</div>',
|
147
|
+
tier_icon,
|
148
|
+
obj.id,
|
149
|
+
short_id
|
74
150
|
)
|
75
151
|
|
76
|
-
@display(description="User")
|
152
|
+
@display(description="User", ordering='user__email')
|
77
153
|
def user_display(self, obj):
|
78
|
-
"""Display user information."""
|
79
|
-
|
80
|
-
'
|
81
|
-
|
82
|
-
|
83
|
-
|
154
|
+
"""Display user information with subscription history."""
|
155
|
+
if obj.user:
|
156
|
+
# Count user's total subscriptions
|
157
|
+
total_subscriptions = Subscription.objects.filter(user=obj.user).count()
|
158
|
+
|
159
|
+
return format_html(
|
160
|
+
'<div>'
|
161
|
+
'<div class="font-medium text-gray-900 dark:text-gray-100">{}</div>'
|
162
|
+
'<div class="text-xs text-gray-500">{}</div>'
|
163
|
+
'<div class="text-xs text-blue-600 dark:text-blue-400">{} subscription{}</div>'
|
164
|
+
'</div>',
|
165
|
+
obj.user.get_full_name() or obj.user.username,
|
166
|
+
obj.user.email,
|
167
|
+
total_subscriptions,
|
168
|
+
's' if total_subscriptions != 1 else ''
|
169
|
+
)
|
170
|
+
return format_html('<span class="text-gray-500">No user</span>')
|
84
171
|
|
85
|
-
@display(description="
|
86
|
-
def endpoint_group_display(self, obj):
|
87
|
-
"""Display endpoint group."""
|
88
|
-
return format_html(
|
89
|
-
'<strong>{}</strong><br><small>{}</small>',
|
90
|
-
obj.endpoint_group.display_name,
|
91
|
-
obj.endpoint_group.description[:40] + '...' if len(obj.endpoint_group.description) > 40 else obj.endpoint_group.description
|
92
|
-
)
|
93
|
-
|
94
|
-
@display(description="Tier")
|
172
|
+
@display(description="Tier", ordering='tier')
|
95
173
|
def tier_display(self, obj):
|
96
|
-
"""Display tier with
|
174
|
+
"""Display subscription tier with pricing."""
|
97
175
|
tier_colors = {
|
98
|
-
'
|
99
|
-
'
|
100
|
-
'
|
176
|
+
'free': 'text-gray-600 dark:text-gray-400',
|
177
|
+
'basic': 'text-yellow-600 dark:text-yellow-400',
|
178
|
+
'pro': 'text-blue-600 dark:text-blue-400',
|
179
|
+
'enterprise': 'text-purple-600 dark:text-purple-400'
|
101
180
|
}
|
102
181
|
|
103
|
-
color = tier_colors.get(obj.tier, '
|
182
|
+
color = tier_colors.get(obj.tier, 'text-gray-600')
|
104
183
|
|
105
184
|
return format_html(
|
106
|
-
'<
|
185
|
+
'<div>'
|
186
|
+
'<div class="font-medium {}">{}</div>'
|
187
|
+
'<div class="text-xs text-gray-500">${}/month</div>'
|
188
|
+
'</div>',
|
107
189
|
color,
|
108
190
|
obj.get_tier_display(),
|
109
|
-
obj.
|
191
|
+
obj.monthly_cost_usd
|
110
192
|
)
|
111
193
|
|
112
|
-
@display(description="Status")
|
194
|
+
@display(description="Status", ordering='status')
|
113
195
|
def status_display(self, obj):
|
114
|
-
"""Display status with
|
115
|
-
|
116
|
-
'
|
117
|
-
'
|
118
|
-
'
|
119
|
-
'
|
120
|
-
'trial': '#17a2b8',
|
196
|
+
"""Display status with expiry warnings."""
|
197
|
+
status_config = {
|
198
|
+
Subscription.SubscriptionStatus.ACTIVE: ('✅', 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', 'Active'),
|
199
|
+
Subscription.SubscriptionStatus.EXPIRED: ('⌛', 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', 'Expired'),
|
200
|
+
Subscription.SubscriptionStatus.CANCELLED: ('🚫', 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200', 'Cancelled'),
|
201
|
+
Subscription.SubscriptionStatus.SUSPENDED: ('⏸️', 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200', 'Suspended'),
|
121
202
|
}
|
122
203
|
|
123
|
-
|
204
|
+
icon, color_class, label = status_config.get(
|
205
|
+
obj.status,
|
206
|
+
('❓', 'bg-gray-100 text-gray-800', 'Unknown')
|
207
|
+
)
|
124
208
|
|
125
|
-
|
126
|
-
'<span
|
127
|
-
|
128
|
-
|
209
|
+
badge = format_html(
|
210
|
+
'<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {}">'
|
211
|
+
'{} {}'
|
212
|
+
'</span>',
|
213
|
+
color_class,
|
214
|
+
icon,
|
215
|
+
label
|
129
216
|
)
|
217
|
+
|
218
|
+
# Add expiry warning if active and expiring soon
|
219
|
+
if obj.status == Subscription.SubscriptionStatus.ACTIVE and obj.expires_at:
|
220
|
+
time_until_expiry = obj.expires_at - timezone.now()
|
221
|
+
if time_until_expiry < timedelta(days=7):
|
222
|
+
warning = format_html(
|
223
|
+
'<div class="text-xs text-orange-600 dark:text-orange-400 mt-1">⚠️ Expires soon</div>'
|
224
|
+
)
|
225
|
+
return format_html('{}<br>{}', badge, warning)
|
226
|
+
|
227
|
+
return badge
|
130
228
|
|
131
229
|
@display(description="Usage")
|
132
230
|
def usage_display(self, obj):
|
133
|
-
"""Display usage with progress."""
|
134
|
-
|
135
|
-
|
231
|
+
"""Display usage statistics with progress bars."""
|
232
|
+
monthly_limit = obj.requests_per_day * 30 # Approximate monthly limit
|
233
|
+
monthly_used = obj.total_requests
|
234
|
+
|
235
|
+
if monthly_limit > 0:
|
236
|
+
usage_percentage = (monthly_used / monthly_limit) * 100
|
237
|
+
|
238
|
+
if usage_percentage >= 90:
|
239
|
+
bar_color = "bg-red-500"
|
240
|
+
text_color = "text-red-600 dark:text-red-400"
|
241
|
+
elif usage_percentage >= 75:
|
242
|
+
bar_color = "bg-orange-500"
|
243
|
+
text_color = "text-orange-600 dark:text-orange-400"
|
244
|
+
else:
|
245
|
+
bar_color = "bg-green-500"
|
246
|
+
text_color = "text-green-600 dark:text-green-400"
|
247
|
+
|
248
|
+
return format_html(
|
249
|
+
'<div class="w-full">'
|
250
|
+
'<div class="flex justify-between text-xs {}">'
|
251
|
+
'<span>{:,} / {:,}</span>'
|
252
|
+
'<span>{:.1f}%</span>'
|
253
|
+
'</div>'
|
254
|
+
'<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700 mt-1">'
|
255
|
+
'<div class="{} h-2 rounded-full" style="width: {}%"></div>'
|
256
|
+
'</div>'
|
257
|
+
'</div>',
|
258
|
+
text_color,
|
259
|
+
monthly_used,
|
260
|
+
monthly_limit,
|
261
|
+
usage_percentage,
|
262
|
+
bar_color,
|
263
|
+
min(usage_percentage, 100)
|
264
|
+
)
|
265
|
+
else:
|
266
|
+
# Unlimited plan
|
267
|
+
return format_html(
|
268
|
+
'<div class="text-center">'
|
269
|
+
'<div class="font-bold text-blue-600 dark:text-blue-400">{:,}</div>'
|
270
|
+
'<div class="text-xs text-gray-500">Total requests</div>'
|
271
|
+
'</div>',
|
272
|
+
monthly_used
|
273
|
+
)
|
274
|
+
|
275
|
+
@display(description="Expiry", ordering='expires_at')
|
276
|
+
def expiry_display(self, obj):
|
277
|
+
"""Display expiry information with countdown."""
|
278
|
+
if not obj.expires_at:
|
279
|
+
return format_html(
|
280
|
+
'<div class="text-center text-blue-600 dark:text-blue-400">'
|
281
|
+
'<div class="font-bold">∞</div>'
|
282
|
+
'<div class="text-xs">Never expires</div>'
|
283
|
+
'</div>'
|
284
|
+
)
|
136
285
|
|
137
|
-
|
286
|
+
now = timezone.now()
|
138
287
|
|
139
|
-
if
|
140
|
-
|
141
|
-
|
142
|
-
|
288
|
+
if obj.expires_at <= now:
|
289
|
+
# Already expired
|
290
|
+
return format_html(
|
291
|
+
'<div class="text-center text-red-600 dark:text-red-400">'
|
292
|
+
'<div class="font-bold">Expired</div>'
|
293
|
+
'<div class="text-xs">{}</div>'
|
294
|
+
'</div>',
|
295
|
+
naturaltime(obj.expires_at)
|
296
|
+
)
|
297
|
+
|
298
|
+
time_remaining = obj.expires_at - now
|
299
|
+
|
300
|
+
if time_remaining < timedelta(days=1):
|
301
|
+
color = "text-red-600 dark:text-red-400"
|
302
|
+
icon = "🚨"
|
303
|
+
elif time_remaining < timedelta(days=7):
|
304
|
+
color = "text-orange-600 dark:text-orange-400"
|
305
|
+
icon = "⚠️"
|
143
306
|
else:
|
144
|
-
color =
|
307
|
+
color = "text-green-600 dark:text-green-400"
|
308
|
+
icon = "✅"
|
145
309
|
|
146
310
|
return format_html(
|
147
|
-
'<
|
311
|
+
'<div class="text-center {}">'
|
312
|
+
'<div><span class="mr-1">{}</span>{}</div>'
|
313
|
+
'<div class="text-xs">{}</div>'
|
314
|
+
'</div>',
|
148
315
|
color,
|
149
|
-
|
150
|
-
obj.
|
151
|
-
|
316
|
+
icon,
|
317
|
+
naturaltime(obj.expires_at),
|
318
|
+
obj.expires_at.strftime('%Y-%m-%d')
|
319
|
+
)
|
320
|
+
|
321
|
+
@display(description="Created", ordering='created_at')
|
322
|
+
def created_at_display(self, obj):
|
323
|
+
"""Display creation date."""
|
324
|
+
return format_html(
|
325
|
+
'<div class="text-xs">'
|
326
|
+
'<div>{}</div>'
|
327
|
+
'<div class="text-gray-500">{}</div>'
|
328
|
+
'</div>',
|
329
|
+
obj.created_at.strftime('%Y-%m-%d'),
|
330
|
+
naturaltime(obj.created_at)
|
331
|
+
)
|
332
|
+
|
333
|
+
def changelist_view(self, request, extra_context=None):
|
334
|
+
"""Add subscription statistics to changelist context."""
|
335
|
+
extra_context = extra_context or {}
|
336
|
+
|
337
|
+
try:
|
338
|
+
# Basic statistics
|
339
|
+
total_subscriptions = Subscription.objects.count()
|
340
|
+
|
341
|
+
# Status distribution
|
342
|
+
status_stats = {}
|
343
|
+
for status in Subscription.SubscriptionStatus:
|
344
|
+
count = Subscription.objects.filter(status=status).count()
|
345
|
+
status_stats[status] = count
|
346
|
+
|
347
|
+
# Tier distribution
|
348
|
+
tier_stats = Subscription.objects.values('tier').annotate(
|
349
|
+
count=Count('id')
|
350
|
+
).order_by('tier')
|
351
|
+
|
352
|
+
# Revenue statistics
|
353
|
+
revenue_stats = Subscription.objects.filter(
|
354
|
+
status=Subscription.SubscriptionStatus.ACTIVE
|
355
|
+
).values('tier').annotate(
|
356
|
+
count=Count('id'),
|
357
|
+
revenue=Sum('monthly_cost_usd')
|
358
|
+
)
|
359
|
+
|
360
|
+
# Expiry alerts
|
361
|
+
now = timezone.now()
|
362
|
+
expiring_soon = Subscription.objects.filter(
|
363
|
+
status=Subscription.SubscriptionStatus.ACTIVE,
|
364
|
+
expires_at__lte=now + timedelta(days=7),
|
365
|
+
expires_at__gt=now
|
366
|
+
).count()
|
367
|
+
|
368
|
+
recently_expired = Subscription.objects.filter(
|
369
|
+
status=Subscription.SubscriptionStatus.EXPIRED,
|
370
|
+
expires_at__gte=now - timedelta(days=7)
|
371
|
+
).count()
|
372
|
+
|
373
|
+
# Usage statistics
|
374
|
+
high_usage_subscriptions = Subscription.objects.filter(
|
375
|
+
status=Subscription.SubscriptionStatus.ACTIVE,
|
376
|
+
total_requests__gte=1000
|
377
|
+
).count()
|
378
|
+
|
379
|
+
extra_context.update({
|
380
|
+
'subscription_stats': {
|
381
|
+
'total_subscriptions': total_subscriptions,
|
382
|
+
'status_stats': status_stats,
|
383
|
+
'tier_stats': tier_stats,
|
384
|
+
'revenue_stats': revenue_stats,
|
385
|
+
'expiring_soon': expiring_soon,
|
386
|
+
'recently_expired': recently_expired,
|
387
|
+
'high_usage_subscriptions': high_usage_subscriptions,
|
388
|
+
}
|
389
|
+
})
|
390
|
+
|
391
|
+
except Exception as e:
|
392
|
+
logger.warning(f"Failed to generate subscription statistics: {e}")
|
393
|
+
extra_context['subscription_stats'] = None
|
394
|
+
|
395
|
+
return super().changelist_view(request, extra_context)
|
396
|
+
|
397
|
+
# ===== ADMIN ACTIONS =====
|
398
|
+
|
399
|
+
@action(
|
400
|
+
description="✅ Activate Subscriptions",
|
401
|
+
icon="play_arrow",
|
402
|
+
variant=ActionVariant.SUCCESS
|
403
|
+
)
|
404
|
+
def activate_subscriptions(self, request, queryset):
|
405
|
+
"""Activate selected subscriptions."""
|
406
|
+
|
407
|
+
activatable = queryset.filter(
|
408
|
+
status__in=[
|
409
|
+
Subscription.SubscriptionStatus.SUSPENDED,
|
410
|
+
Subscription.SubscriptionStatus.CANCELLED
|
411
|
+
]
|
152
412
|
)
|
413
|
+
|
414
|
+
activated_count = 0
|
415
|
+
|
416
|
+
for subscription in activatable:
|
417
|
+
try:
|
418
|
+
subscription.activate()
|
419
|
+
activated_count += 1
|
420
|
+
|
421
|
+
except Exception as e:
|
422
|
+
logger.error(f"Failed to activate subscription {subscription.id}: {e}")
|
423
|
+
|
424
|
+
if activated_count > 0:
|
425
|
+
messages.success(
|
426
|
+
request,
|
427
|
+
f"✅ Activated {activated_count} subscriptions"
|
428
|
+
)
|
429
|
+
|
430
|
+
skipped = queryset.count() - activated_count
|
431
|
+
if skipped > 0:
|
432
|
+
messages.info(
|
433
|
+
request,
|
434
|
+
f"ℹ️ Skipped {skipped} subscriptions (already active or expired)"
|
435
|
+
)
|
153
436
|
|
154
|
-
@
|
155
|
-
|
156
|
-
""
|
157
|
-
|
158
|
-
|
159
|
-
|
437
|
+
@action(
|
438
|
+
description="⏸️ Suspend Subscriptions",
|
439
|
+
icon="pause",
|
440
|
+
variant=ActionVariant.WARNING
|
441
|
+
)
|
442
|
+
def suspend_subscriptions(self, request, queryset):
|
443
|
+
"""Suspend selected subscriptions."""
|
444
|
+
|
445
|
+
suspendable = queryset.filter(
|
446
|
+
status=Subscription.SubscriptionStatus.ACTIVE
|
447
|
+
)
|
448
|
+
|
449
|
+
suspended_count = 0
|
450
|
+
|
451
|
+
for subscription in suspendable:
|
452
|
+
try:
|
453
|
+
subscription.suspend(reason=f"Suspended by admin {request.user.username}")
|
454
|
+
suspended_count += 1
|
455
|
+
|
456
|
+
except Exception as e:
|
457
|
+
logger.error(f"Failed to suspend subscription {subscription.id}: {e}")
|
458
|
+
|
459
|
+
if suspended_count > 0:
|
460
|
+
messages.success(
|
461
|
+
request,
|
462
|
+
f"⏸️ Suspended {suspended_count} subscriptions"
|
463
|
+
)
|
464
|
+
|
465
|
+
skipped = queryset.count() - suspended_count
|
466
|
+
if skipped > 0:
|
467
|
+
messages.info(
|
468
|
+
request,
|
469
|
+
f"ℹ️ Skipped {skipped} subscriptions (not active)"
|
470
|
+
)
|
471
|
+
|
472
|
+
@action(
|
473
|
+
description="📅 Extend Subscriptions (30 days)",
|
474
|
+
icon="schedule",
|
475
|
+
variant=ActionVariant.INFO
|
476
|
+
)
|
477
|
+
def extend_subscriptions(self, request, queryset):
|
478
|
+
"""Extend selected subscriptions by 30 days."""
|
479
|
+
|
480
|
+
extendable = queryset.filter(
|
481
|
+
status__in=[
|
482
|
+
Subscription.SubscriptionStatus.ACTIVE,
|
483
|
+
Subscription.SubscriptionStatus.EXPIRED
|
484
|
+
]
|
485
|
+
)
|
486
|
+
|
487
|
+
extended_count = 0
|
488
|
+
|
489
|
+
for subscription in extendable:
|
490
|
+
try:
|
491
|
+
subscription.renew(duration_days=30)
|
492
|
+
extended_count += 1
|
493
|
+
|
494
|
+
except Exception as e:
|
495
|
+
logger.error(f"Failed to extend subscription {subscription.id}: {e}")
|
496
|
+
|
497
|
+
if extended_count > 0:
|
498
|
+
messages.success(
|
499
|
+
request,
|
500
|
+
f"📅 Extended {extended_count} subscriptions by 30 days"
|
501
|
+
)
|
502
|
+
|
503
|
+
skipped = queryset.count() - extended_count
|
504
|
+
if skipped > 0:
|
505
|
+
messages.info(
|
506
|
+
request,
|
507
|
+
f"ℹ️ Skipped {skipped} subscriptions (cancelled or suspended)"
|
508
|
+
)
|
160
509
|
|
161
510
|
|
162
511
|
@admin.register(EndpointGroup)
|
@@ -165,63 +514,164 @@ class EndpointGroupAdmin(ModelAdmin):
|
|
165
514
|
|
166
515
|
list_display = [
|
167
516
|
'name',
|
168
|
-
'
|
169
|
-
'
|
170
|
-
'limits_display',
|
171
|
-
'is_active',
|
517
|
+
'description',
|
518
|
+
'tariff_count_display',
|
172
519
|
'created_at_display'
|
173
520
|
]
|
174
521
|
|
175
|
-
|
522
|
+
search_fields = ['name', 'description']
|
176
523
|
|
177
|
-
|
524
|
+
readonly_fields = ['created_at', 'updated_at']
|
178
525
|
|
179
|
-
|
526
|
+
@display(description="Tariffs")
|
527
|
+
def tariff_count_display(self, obj):
|
528
|
+
"""Display tariff count."""
|
529
|
+
count = obj.tariffendpointgroup_set.count()
|
530
|
+
return format_html(
|
531
|
+
'<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">'
|
532
|
+
'{} tariff{}'
|
533
|
+
'</span>',
|
534
|
+
count,
|
535
|
+
's' if count != 1 else ''
|
536
|
+
)
|
180
537
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
538
|
+
@display(description="Created", ordering='created_at')
|
539
|
+
def created_at_display(self, obj):
|
540
|
+
"""Display creation date."""
|
541
|
+
return naturaltime(obj.created_at)
|
542
|
+
|
543
|
+
|
544
|
+
@admin.register(Tariff)
|
545
|
+
class TariffAdmin(ModelAdmin):
|
546
|
+
"""Admin interface for tariffs with endpoint group management."""
|
547
|
+
|
548
|
+
list_display = [
|
549
|
+
'name',
|
550
|
+
'tier_display',
|
551
|
+
'price_display',
|
552
|
+
'endpoint_groups_display',
|
553
|
+
'subscription_count_display',
|
554
|
+
'is_active'
|
194
555
|
]
|
195
556
|
|
196
|
-
|
197
|
-
|
198
|
-
|
557
|
+
list_filter = ['is_active', 'is_public', 'created_at']
|
558
|
+
|
559
|
+
search_fields = ['name', 'description']
|
560
|
+
|
561
|
+
readonly_fields = ['created_at', 'updated_at']
|
562
|
+
|
563
|
+
inlines = [TariffEndpointGroupInline]
|
564
|
+
|
565
|
+
@display(description="Tier", ordering='tier')
|
566
|
+
def tier_display(self, obj):
|
567
|
+
"""Display tier with badge."""
|
568
|
+
tier_config = {
|
569
|
+
'free': ('🆓', 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'),
|
570
|
+
'basic': ('🥉', 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'),
|
571
|
+
'premium': ('🥈', 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'),
|
572
|
+
'enterprise': ('🥇', 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'),
|
573
|
+
}
|
574
|
+
|
575
|
+
icon, color_class = tier_config.get(obj.tier, ('📋', 'bg-gray-100 text-gray-800'))
|
576
|
+
|
199
577
|
return format_html(
|
200
|
-
'<
|
201
|
-
'
|
202
|
-
'
|
203
|
-
|
204
|
-
|
205
|
-
obj.
|
206
|
-
obj.premium_price,
|
207
|
-
obj.enterprise_price
|
578
|
+
'<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {}">'
|
579
|
+
'{} {}'
|
580
|
+
'</span>',
|
581
|
+
color_class,
|
582
|
+
icon,
|
583
|
+
obj.tier.title()
|
208
584
|
)
|
209
585
|
|
210
|
-
@display(description="
|
211
|
-
def
|
212
|
-
"""Display
|
586
|
+
@display(description="Price", ordering='monthly_price_usd')
|
587
|
+
def price_display(self, obj):
|
588
|
+
"""Display price with formatting."""
|
589
|
+
if obj.monthly_price_usd == 0:
|
590
|
+
return format_html(
|
591
|
+
'<span class="font-bold text-green-600 dark:text-green-400">FREE</span>'
|
592
|
+
)
|
593
|
+
else:
|
594
|
+
return format_html(
|
595
|
+
'<span class="font-bold text-blue-600 dark:text-blue-400">${}/month</span>',
|
596
|
+
obj.monthly_price_usd
|
597
|
+
)
|
598
|
+
|
599
|
+
@display(description="Endpoint Groups")
|
600
|
+
def endpoint_groups_display(self, obj):
|
601
|
+
"""Display endpoint groups count."""
|
602
|
+
count = obj.endpoint_groups.count()
|
213
603
|
return format_html(
|
214
|
-
'<
|
215
|
-
'
|
216
|
-
'
|
217
|
-
|
604
|
+
'<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">'
|
605
|
+
'{} group{}'
|
606
|
+
'</span>',
|
607
|
+
count,
|
608
|
+
's' if count != 1 else ''
|
609
|
+
)
|
610
|
+
|
611
|
+
@display(description="Subscriptions")
|
612
|
+
def subscription_count_display(self, obj):
|
613
|
+
"""Display active subscription count."""
|
614
|
+
count = obj.subscription_set.filter(
|
615
|
+
status=Subscription.SubscriptionStatus.ACTIVE
|
616
|
+
).count()
|
617
|
+
|
618
|
+
if count > 0:
|
619
|
+
return format_html(
|
620
|
+
'<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">'
|
621
|
+
'{} active'
|
622
|
+
'</span>',
|
623
|
+
count
|
624
|
+
)
|
625
|
+
|
626
|
+
return format_html(
|
627
|
+
'<span class="text-gray-500">No active</span>'
|
628
|
+
)
|
629
|
+
|
630
|
+
|
631
|
+
@admin.register(TariffEndpointGroup)
|
632
|
+
class TariffEndpointGroupAdmin(ModelAdmin):
|
633
|
+
"""Admin interface for tariff endpoint group relationships."""
|
634
|
+
|
635
|
+
list_display = [
|
636
|
+
'tariff_display',
|
637
|
+
'endpoint_group_display',
|
638
|
+
'custom_rate_limit_display',
|
639
|
+
'is_enabled'
|
640
|
+
]
|
641
|
+
|
642
|
+
list_filter = ['is_enabled', 'endpoint_group']
|
643
|
+
|
644
|
+
search_fields = [
|
645
|
+
'tariff__name',
|
646
|
+
'endpoint_group__name'
|
647
|
+
]
|
648
|
+
|
649
|
+
@display(description="Tariff", ordering='tariff__name')
|
650
|
+
def tariff_display(self, obj):
|
651
|
+
"""Display tariff with tier."""
|
652
|
+
return format_html(
|
653
|
+
'<div>'
|
654
|
+
'<div class="font-medium">{}</div>'
|
655
|
+
'<div class="text-xs text-gray-500">${}/month</div>'
|
218
656
|
'</div>',
|
219
|
-
obj.
|
220
|
-
obj.
|
221
|
-
obj.enterprise_limit if obj.enterprise_limit > 0 else '∞'
|
657
|
+
obj.tariff.name,
|
658
|
+
obj.tariff.monthly_price_usd
|
222
659
|
)
|
223
660
|
|
224
|
-
@display(description="
|
225
|
-
def
|
226
|
-
"""Display
|
227
|
-
return
|
661
|
+
@display(description="Endpoint Group", ordering='endpoint_group__name')
|
662
|
+
def endpoint_group_display(self, obj):
|
663
|
+
"""Display endpoint group."""
|
664
|
+
return obj.endpoint_group.name
|
665
|
+
|
666
|
+
@display(description="Custom Rate Limit", ordering='custom_rate_limit')
|
667
|
+
def custom_rate_limit_display(self, obj):
|
668
|
+
"""Display custom rate limit."""
|
669
|
+
if obj.custom_rate_limit:
|
670
|
+
return format_html(
|
671
|
+
'<span class="font-mono text-orange-600 dark:text-orange-400">{:,}/hour</span>',
|
672
|
+
obj.custom_rate_limit
|
673
|
+
)
|
674
|
+
else:
|
675
|
+
return format_html(
|
676
|
+
'<span class="text-gray-500">Use tariff default</span>'
|
677
|
+
)
|