django-cfg 1.3.7__py3-none-any.whl → 1.3.9__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/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +258 -0
- django_cfg/apps/payments/admin/payments_admin.py +171 -461
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +1 -1
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +95 -39
- django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
- django_cfg/apps/payments/services/providers/models/base.py +122 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -32
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
- django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +5 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +4 -3
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/app_agent_diagnose.py +470 -0
- django_cfg/management/commands/app_agent_generate.py +342 -0
- django_cfg/management/commands/app_agent_info.py +308 -0
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_app_agent/__init__.py +87 -0
- django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
- django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
- django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
- django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
- django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
- django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
- django_cfg/modules/django_app_agent/core/__init__.py +33 -0
- django_cfg/modules/django_app_agent/core/config.py +300 -0
- django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
- django_cfg/modules/django_app_agent/models/__init__.py +71 -0
- django_cfg/modules/django_app_agent/models/base.py +283 -0
- django_cfg/modules/django_app_agent/models/context.py +496 -0
- django_cfg/modules/django_app_agent/models/enums.py +481 -0
- django_cfg/modules/django_app_agent/models/requests.py +500 -0
- django_cfg/modules/django_app_agent/models/responses.py +585 -0
- django_cfg/modules/django_app_agent/pytest.ini +6 -0
- django_cfg/modules/django_app_agent/services/__init__.py +42 -0
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
- django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
- django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
- django_cfg/modules/django_app_agent/services/base.py +437 -0
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
- django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
- django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
- django_cfg/modules/django_app_agent/services/report_service.py +332 -0
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
- django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
- django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
- django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
- django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
- django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
- django_cfg/modules/django_app_agent/ui/cli.py +419 -0
- django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
- django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
- django_cfg/modules/django_app_agent/utils/logging.py +360 -0
- django_cfg/modules/django_app_agent/utils/validation.py +417 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +3 -0
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,677 +1,160 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
Subscriptions Admin interface using Django Admin Utilities.
|
3
3
|
|
4
|
-
|
4
|
+
Clean subscription management with plan icons and status tracking.
|
5
5
|
"""
|
6
6
|
|
7
7
|
from django.contrib import admin
|
8
|
-
from django.
|
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
|
8
|
+
from django.db.models import Count
|
9
|
+
from unfold.admin import ModelAdmin
|
17
10
|
|
18
|
-
from
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
11
|
+
from django_cfg.modules.django_admin import (
|
12
|
+
OptimizedModelAdmin,
|
13
|
+
DisplayMixin,
|
14
|
+
MoneyDisplayConfig,
|
15
|
+
StatusBadgeConfig,
|
16
|
+
DateTimeDisplayConfig,
|
17
|
+
Icons,
|
18
|
+
ActionVariant,
|
19
|
+
display,
|
20
|
+
action
|
21
|
+
)
|
22
|
+
from django_cfg.modules.django_admin.utils.badges import StatusBadge
|
24
23
|
from django_cfg.modules.django_logger import get_logger
|
24
|
+
from ..models import Subscription
|
25
25
|
|
26
26
|
logger = get_logger("subscriptions_admin")
|
27
27
|
|
28
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']
|
34
|
-
|
35
|
-
|
36
29
|
@admin.register(Subscription)
|
37
|
-
class SubscriptionAdmin(ModelAdmin):
|
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
|
-
"""
|
30
|
+
class SubscriptionAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin):
|
31
|
+
"""Subscription admin using Django Admin Utilities with plan icons."""
|
48
32
|
|
49
|
-
|
50
|
-
change_list_template = 'admin/payments/subscription/change_list.html'
|
33
|
+
select_related_fields = ['user']
|
51
34
|
|
52
35
|
list_display = [
|
53
|
-
'subscription_display',
|
54
36
|
'user_display',
|
55
|
-
'
|
37
|
+
'plan_display',
|
38
|
+
'amount_display',
|
56
39
|
'status_display',
|
57
|
-
'
|
58
|
-
'expiry_display',
|
59
|
-
'created_at_display'
|
60
|
-
]
|
61
|
-
|
62
|
-
list_display_links = ['subscription_display']
|
63
|
-
|
64
|
-
search_fields = [
|
65
|
-
'id',
|
66
|
-
'user__email',
|
67
|
-
'user__username',
|
68
|
-
'tier'
|
69
|
-
]
|
70
|
-
|
71
|
-
list_filter = [
|
72
|
-
SubscriptionStatusFilter,
|
73
|
-
SubscriptionTierFilter,
|
74
|
-
RecentActivityFilter,
|
75
|
-
'created_at',
|
76
|
-
'expires_at'
|
77
|
-
]
|
78
|
-
|
79
|
-
readonly_fields = [
|
80
|
-
'id',
|
81
|
-
'created_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',
|
40
|
+
'expires_display'
|
91
41
|
]
|
92
42
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
'id',
|
97
|
-
'user',
|
98
|
-
'tier',
|
99
|
-
'status'
|
100
|
-
]
|
101
|
-
}),
|
102
|
-
('Usage & Limits', {
|
103
|
-
'fields': [
|
104
|
-
'total_requests',
|
105
|
-
'requests_per_hour',
|
106
|
-
'requests_per_day',
|
107
|
-
'last_request_at'
|
108
|
-
]
|
109
|
-
}),
|
110
|
-
('Billing & Expiry', {
|
111
|
-
'fields': [
|
112
|
-
'monthly_cost_usd',
|
113
|
-
'starts_at',
|
114
|
-
'expires_at',
|
115
|
-
'auto_renew'
|
116
|
-
]
|
117
|
-
}),
|
118
|
-
('Timestamps', {
|
119
|
-
'fields': ['created_at', 'updated_at'],
|
120
|
-
'classes': ['collapse']
|
121
|
-
})
|
122
|
-
]
|
43
|
+
list_filter = ['status', 'tier', 'created_at']
|
44
|
+
search_fields = ['user__username', 'user__email']
|
45
|
+
readonly_fields = ['created_at', 'updated_at']
|
123
46
|
|
124
|
-
|
125
|
-
|
126
|
-
return super().get_queryset(request).select_related('user').prefetch_related('endpoint_groups')
|
47
|
+
# Register actions
|
48
|
+
actions = ['activate_subscriptions', 'cancel_subscriptions', 'extend_trial']
|
127
49
|
|
128
|
-
@display(description="
|
129
|
-
def subscription_display(self, obj):
|
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
|
-
|
142
|
-
return format_html(
|
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
|
150
|
-
)
|
151
|
-
|
152
|
-
@display(description="User", ordering='user__email')
|
50
|
+
@display(description="User", header=True)
|
153
51
|
def user_display(self, obj):
|
154
|
-
"""
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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>')
|
171
|
-
|
172
|
-
@display(description="Tier", ordering='tier')
|
173
|
-
def tier_display(self, obj):
|
174
|
-
"""Display subscription tier with pricing."""
|
175
|
-
tier_colors = {
|
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'
|
52
|
+
"""User display with avatar."""
|
53
|
+
return self.display_user_with_avatar(obj, 'user')
|
54
|
+
|
55
|
+
@display(description="Plan")
|
56
|
+
def plan_display(self, obj):
|
57
|
+
"""Plan display with tier-specific icons."""
|
58
|
+
# Plan type to icon and variant mapping
|
59
|
+
plan_config = {
|
60
|
+
'basic': {'variant': 'secondary', 'icon': Icons.PERSON},
|
61
|
+
'premium': {'variant': 'primary', 'icon': Icons.STAR},
|
62
|
+
'enterprise': {'variant': 'success', 'icon': Icons.BUSINESS},
|
63
|
+
'pro': {'variant': 'info', 'icon': Icons.WORKSPACE_PREMIUM},
|
180
64
|
}
|
181
65
|
|
182
|
-
|
66
|
+
tier = getattr(obj, 'tier', '').lower()
|
67
|
+
config_data = plan_config.get(tier, {'variant': 'info', 'icon': Icons.SUBSCRIPTIONS})
|
183
68
|
|
184
|
-
|
185
|
-
'<div>'
|
186
|
-
'<div class="font-medium {}">{}</div>'
|
187
|
-
'<div class="text-xs text-gray-500">${}/month</div>'
|
188
|
-
'</div>',
|
189
|
-
color,
|
190
|
-
obj.get_tier_display(),
|
191
|
-
obj.monthly_cost_usd
|
192
|
-
)
|
193
|
-
|
194
|
-
@display(description="Status", ordering='status')
|
195
|
-
def status_display(self, obj):
|
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'),
|
202
|
-
}
|
69
|
+
tier_name = obj.get_tier_display() if hasattr(obj, 'get_tier_display') else obj.tier.title()
|
203
70
|
|
204
|
-
|
205
|
-
|
206
|
-
|
71
|
+
badge_config = StatusBadgeConfig(
|
72
|
+
show_icons=True,
|
73
|
+
icon=config_data['icon']
|
207
74
|
)
|
208
75
|
|
209
|
-
|
210
|
-
|
211
|
-
'
|
212
|
-
|
213
|
-
color_class,
|
214
|
-
icon,
|
215
|
-
label
|
76
|
+
return StatusBadge.create(
|
77
|
+
text=tier_name,
|
78
|
+
variant=config_data['variant'],
|
79
|
+
config=badge_config
|
216
80
|
)
|
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
|
228
81
|
|
229
|
-
@display(description="
|
230
|
-
def
|
231
|
-
"""
|
232
|
-
|
233
|
-
|
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
|
-
)
|
82
|
+
@display(description="Amount")
|
83
|
+
def amount_display(self, obj):
|
84
|
+
"""Amount display with currency."""
|
85
|
+
config = MoneyDisplayConfig(currency="USD", show_sign=False)
|
86
|
+
return self.display_money_amount(obj, 'monthly_cost_usd', config)
|
274
87
|
|
275
|
-
@display(description="
|
276
|
-
def
|
277
|
-
"""
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
now = timezone.now()
|
287
|
-
|
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
|
-
)
|
88
|
+
@display(description="Status", label=True)
|
89
|
+
def status_display(self, obj):
|
90
|
+
"""Status display with subscription-specific icons."""
|
91
|
+
subscription_mappings = {
|
92
|
+
'active': 'success',
|
93
|
+
'expired': 'danger',
|
94
|
+
'cancelled': 'secondary',
|
95
|
+
'pending': 'warning',
|
96
|
+
'trial': 'info'
|
97
|
+
}
|
297
98
|
|
298
|
-
|
99
|
+
# Status-specific icons
|
100
|
+
status_icons = {
|
101
|
+
'active': Icons.CHECK_CIRCLE,
|
102
|
+
'expired': Icons.SCHEDULE,
|
103
|
+
'cancelled': Icons.CANCEL,
|
104
|
+
'pending': Icons.PENDING,
|
105
|
+
'trial': Icons.TIMER
|
106
|
+
}
|
299
107
|
|
300
|
-
|
301
|
-
|
302
|
-
icon = "🚨"
|
303
|
-
elif time_remaining < timedelta(days=7):
|
304
|
-
color = "text-orange-600 dark:text-orange-400"
|
305
|
-
icon = "⚠️"
|
306
|
-
else:
|
307
|
-
color = "text-green-600 dark:text-green-400"
|
308
|
-
icon = "✅"
|
108
|
+
status = getattr(obj, 'status', 'unknown').lower()
|
109
|
+
icon = status_icons.get(status, Icons.HELP)
|
309
110
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
'</div>',
|
315
|
-
color,
|
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)
|
111
|
+
config = StatusBadgeConfig(
|
112
|
+
custom_mappings=subscription_mappings,
|
113
|
+
show_icons=True,
|
114
|
+
icon=icon
|
331
115
|
)
|
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
116
|
|
395
|
-
return
|
117
|
+
return self.display_status_auto(obj, 'status', config)
|
396
118
|
|
397
|
-
|
119
|
+
@display(description="Expires")
|
120
|
+
def expires_display(self, obj):
|
121
|
+
"""Expiry date display."""
|
122
|
+
if not hasattr(obj, 'expires_at') or not obj.expires_at:
|
123
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.ALL_INCLUSIVE)
|
124
|
+
return StatusBadge.create(text="No Expiry", variant="info", config=config)
|
125
|
+
|
126
|
+
return self.display_datetime_relative(obj, 'expires_at')
|
398
127
|
|
399
|
-
@action(
|
400
|
-
description="✅ Activate Subscriptions",
|
401
|
-
icon="play_arrow",
|
402
|
-
variant=ActionVariant.SUCCESS
|
403
|
-
)
|
128
|
+
@action(description="Activate subscriptions", variant=ActionVariant.SUCCESS)
|
404
129
|
def activate_subscriptions(self, request, queryset):
|
405
130
|
"""Activate selected subscriptions."""
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
131
|
+
updated = queryset.update(status='active')
|
132
|
+
self.message_user(request, f"Activated {updated} subscription(s).", level='SUCCESS')
|
133
|
+
|
134
|
+
@action(description="Cancel subscriptions", variant=ActionVariant.WARNING)
|
135
|
+
def cancel_subscriptions(self, request, queryset):
|
136
|
+
"""Cancel selected subscriptions."""
|
137
|
+
updated = queryset.update(status='cancelled')
|
138
|
+
self.message_user(request, f"Cancelled {updated} subscription(s).", level='WARNING')
|
139
|
+
|
140
|
+
@action(description="Extend trial period", variant=ActionVariant.INFO)
|
141
|
+
def extend_trial(self, request, queryset):
|
142
|
+
"""Extend trial period for selected subscriptions."""
|
143
|
+
from django.utils import timezone
|
144
|
+
from datetime import timedelta
|
145
|
+
|
146
|
+
trial_subs = queryset.filter(status='trial')
|
147
|
+
updated_count = 0
|
148
|
+
|
149
|
+
for sub in trial_subs:
|
150
|
+
if hasattr(sub, 'expires_at') and sub.expires_at:
|
151
|
+
# Extend by 7 days
|
152
|
+
sub.expires_at = sub.expires_at + timedelta(days=7)
|
153
|
+
sub.save()
|
154
|
+
updated_count += 1
|
155
|
+
|
156
|
+
self.message_user(
|
157
|
+
request,
|
158
|
+
f"Extended trial period for {updated_count} subscription(s).",
|
159
|
+
level='INFO'
|
412
160
|
)
|
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
|
-
)
|
436
|
-
|
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
|
-
)
|
509
|
-
|
510
|
-
|
511
|
-
@admin.register(EndpointGroup)
|
512
|
-
class EndpointGroupAdmin(ModelAdmin):
|
513
|
-
"""Admin interface for endpoint groups."""
|
514
|
-
|
515
|
-
list_display = [
|
516
|
-
'name',
|
517
|
-
'description',
|
518
|
-
'tariff_count_display',
|
519
|
-
'created_at_display'
|
520
|
-
]
|
521
|
-
|
522
|
-
search_fields = ['name', 'description']
|
523
|
-
|
524
|
-
readonly_fields = ['created_at', 'updated_at']
|
525
|
-
|
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
|
-
)
|
537
|
-
|
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'
|
555
|
-
]
|
556
|
-
|
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
|
-
|
577
|
-
return format_html(
|
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()
|
584
|
-
)
|
585
|
-
|
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()
|
603
|
-
return format_html(
|
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>'
|
656
|
-
'</div>',
|
657
|
-
obj.tariff.name,
|
658
|
-
obj.tariff.monthly_price_usd
|
659
|
-
)
|
660
|
-
|
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
|
-
)
|