django-cfg 1.3.5__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.
Files changed (252) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +258 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +171 -461
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
  48. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  49. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
  50. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  51. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  52. django_cfg/apps/payments/middleware/api_access.py +32 -6
  53. django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
  54. django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
  55. django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
  56. django_cfg/apps/payments/models/balance.py +12 -0
  57. django_cfg/apps/payments/models/currencies.py +106 -32
  58. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  59. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  60. django_cfg/apps/payments/services/core/payment_service.py +1 -1
  61. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  62. django_cfg/apps/payments/services/providers/base.py +95 -39
  63. django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
  64. django_cfg/apps/payments/services/providers/models/base.py +122 -0
  65. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  66. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  67. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  68. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  69. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  70. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  71. django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
  72. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  73. django_cfg/apps/payments/services/providers/registry.py +4 -32
  74. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  75. django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
  76. django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
  77. django_cfg/apps/payments/tasks/__init__.py +39 -0
  78. django_cfg/apps/payments/tasks/types.py +73 -0
  79. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  80. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  81. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  82. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  83. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  84. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  85. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  86. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  87. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  88. django_cfg/apps/payments/urls_admin.py +1 -1
  89. django_cfg/apps/payments/views/api/currencies.py +5 -5
  90. django_cfg/apps/payments/views/overview/services.py +2 -2
  91. django_cfg/apps/payments/views/serializers/currencies.py +4 -3
  92. django_cfg/apps/support/admin/__init__.py +10 -1
  93. django_cfg/apps/support/admin/support_admin.py +338 -141
  94. django_cfg/apps/tasks/admin/__init__.py +11 -0
  95. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  96. django_cfg/apps/urls.py +1 -2
  97. django_cfg/config.py +1 -1
  98. django_cfg/core/config.py +10 -5
  99. django_cfg/core/generation.py +1 -1
  100. django_cfg/management/commands/__init__.py +13 -1
  101. django_cfg/management/commands/app_agent_diagnose.py +470 -0
  102. django_cfg/management/commands/app_agent_generate.py +342 -0
  103. django_cfg/management/commands/app_agent_info.py +308 -0
  104. django_cfg/management/commands/migrate_all.py +9 -3
  105. django_cfg/management/commands/migrator.py +11 -6
  106. django_cfg/management/commands/rundramatiq.py +3 -2
  107. django_cfg/middleware/__init__.py +0 -2
  108. django_cfg/models/api_keys.py +115 -0
  109. django_cfg/modules/django_admin/__init__.py +64 -0
  110. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  111. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  112. django_cfg/modules/django_admin/decorators/display.py +106 -0
  113. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  114. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  115. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  116. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  117. django_cfg/modules/django_admin/models/__init__.py +20 -0
  118. django_cfg/modules/django_admin/models/action_models.py +33 -0
  119. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  120. django_cfg/modules/django_admin/models/base.py +26 -0
  121. django_cfg/modules/django_admin/models/display_models.py +31 -0
  122. django_cfg/modules/django_admin/utils/badges.py +159 -0
  123. django_cfg/modules/django_admin/utils/displays.py +247 -0
  124. django_cfg/modules/django_app_agent/__init__.py +87 -0
  125. django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
  126. django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
  127. django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
  128. django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
  129. django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
  130. django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
  131. django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
  132. django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
  133. django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
  134. django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
  135. django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
  136. django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
  137. django_cfg/modules/django_app_agent/core/__init__.py +33 -0
  138. django_cfg/modules/django_app_agent/core/config.py +300 -0
  139. django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
  140. django_cfg/modules/django_app_agent/models/__init__.py +71 -0
  141. django_cfg/modules/django_app_agent/models/base.py +283 -0
  142. django_cfg/modules/django_app_agent/models/context.py +496 -0
  143. django_cfg/modules/django_app_agent/models/enums.py +481 -0
  144. django_cfg/modules/django_app_agent/models/requests.py +500 -0
  145. django_cfg/modules/django_app_agent/models/responses.py +585 -0
  146. django_cfg/modules/django_app_agent/pytest.ini +6 -0
  147. django_cfg/modules/django_app_agent/services/__init__.py +42 -0
  148. django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
  149. django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
  150. django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
  151. django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
  152. django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
  153. django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
  154. django_cfg/modules/django_app_agent/services/base.py +437 -0
  155. django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
  156. django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
  157. django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
  158. django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
  159. django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
  160. django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
  161. django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
  162. django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
  163. django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
  164. django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
  165. django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
  166. django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
  167. django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
  168. django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
  169. django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
  170. django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
  171. django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
  172. django_cfg/modules/django_app_agent/services/report_service.py +332 -0
  173. django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
  174. django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
  175. django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
  176. django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
  177. django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
  178. django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
  179. django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
  180. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
  181. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
  182. django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
  183. django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
  184. django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
  185. django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
  186. django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
  187. django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
  188. django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
  189. django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
  190. django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
  191. django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
  192. django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
  193. django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
  194. django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
  195. django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
  196. django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
  197. django_cfg/modules/django_app_agent/ui/cli.py +419 -0
  198. django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
  199. django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
  200. django_cfg/modules/django_app_agent/utils/logging.py +360 -0
  201. django_cfg/modules/django_app_agent/utils/validation.py +417 -0
  202. django_cfg/modules/django_currency/__init__.py +2 -2
  203. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  204. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  205. django_cfg/modules/django_currency/core/converter.py +12 -12
  206. django_cfg/modules/django_currency/database/__init__.py +2 -2
  207. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  208. django_cfg/modules/django_llm/llm/client.py +10 -2
  209. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  210. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  211. django_cfg/modules/django_unfold/dashboard.py +14 -13
  212. django_cfg/modules/django_unfold/models/config.py +1 -1
  213. django_cfg/registry/core.py +3 -0
  214. django_cfg/registry/third_party.py +2 -2
  215. django_cfg/template_archive/django_sample.zip +0 -0
  216. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
  217. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/RECORD +224 -118
  218. django_cfg/apps/accounts/admin/activity.py +0 -96
  219. django_cfg/apps/accounts/admin/group.py +0 -17
  220. django_cfg/apps/accounts/admin/otp.py +0 -59
  221. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  222. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  223. django_cfg/apps/accounts/admin/user.py +0 -300
  224. django_cfg/apps/agents/core/agent.py +0 -281
  225. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  226. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  227. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  228. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  229. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  230. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  231. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  232. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  233. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  234. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  235. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  236. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  237. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  238. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  239. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  240. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  241. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  242. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  243. django_cfg/apps/tasks/admin.py +0 -320
  244. django_cfg/middleware/static_nocache.py +0 -55
  245. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  246. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  247. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  248. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  249. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  250. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
  251. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
  252. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,677 +1,160 @@
1
1
  """
2
- Subscription Admin interfaces with Unfold integration.
2
+ Subscriptions Admin interface using Django Admin Utilities.
3
3
 
4
- Advanced subscription lifecycle management and monitoring.
4
+ Clean subscription management with plan icons and status tracking.
5
5
  """
6
6
 
7
7
  from django.contrib import admin
8
- from django.utils.html import format_html
9
- from django.contrib.humanize.templatetags.humanize import naturaltime
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 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
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
- # Custom template for subscription statistics
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
- 'tier_display',
37
+ 'plan_display',
38
+ 'amount_display',
56
39
  'status_display',
57
- 'usage_display',
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
- fieldsets = [
94
- ('Subscription Information', {
95
- 'fields': [
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
- def get_queryset(self, request):
125
- """Optimize queryset with related data."""
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="Subscription", ordering='id')
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
- """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>')
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
- color = tier_colors.get(obj.tier, 'text-gray-600')
66
+ tier = getattr(obj, 'tier', '').lower()
67
+ config_data = plan_config.get(tier, {'variant': 'info', 'icon': Icons.SUBSCRIPTIONS})
183
68
 
184
- return format_html(
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
- icon, color_class, label = status_config.get(
205
- obj.status,
206
- ('', 'bg-gray-100 text-gray-800', 'Unknown')
71
+ badge_config = StatusBadgeConfig(
72
+ show_icons=True,
73
+ icon=config_data['icon']
207
74
  )
208
75
 
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
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="Usage")
230
- def usage_display(self, obj):
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
- )
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="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
- )
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
- time_remaining = obj.expires_at - now
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
- 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 = "⚠️"
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
- return format_html(
311
- '<div class="text-center {}">'
312
- '<div><span class="mr-1">{}</span>{}</div>'
313
- '<div class="text-xs">{}</div>'
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 super().changelist_view(request, extra_context)
117
+ return self.display_status_auto(obj, 'status', config)
396
118
 
397
- # ===== ADMIN ACTIONS =====
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
- activatable = queryset.filter(
408
- status__in=[
409
- Subscription.SubscriptionStatus.SUSPENDED,
410
- Subscription.SubscriptionStatus.CANCELLED
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
- )