django-cfg 1.3.7__py3-none-any.whl → 1.3.11__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 (246) 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 +269 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +183 -460
  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 +153 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
  48. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
  49. django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
  50. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
  51. django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
  52. django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
  53. django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
  54. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
  55. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  56. django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
  57. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
  58. django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
  59. django_cfg/apps/payments/config/__init__.py +14 -15
  60. django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
  61. django_cfg/apps/payments/config/helpers.py +8 -13
  62. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  63. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  64. django_cfg/apps/payments/middleware/api_access.py +32 -6
  65. django_cfg/apps/payments/migrations/0001_initial.py +33 -46
  66. django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
  67. django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
  68. django_cfg/apps/payments/models/balance.py +12 -0
  69. django_cfg/apps/payments/models/currencies.py +106 -32
  70. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  71. django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
  72. django_cfg/apps/payments/models/payments.py +94 -0
  73. django_cfg/apps/payments/services/core/base.py +4 -4
  74. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  75. django_cfg/apps/payments/services/core/payment_service.py +266 -39
  76. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  77. django_cfg/apps/payments/services/providers/base.py +303 -41
  78. django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
  79. django_cfg/apps/payments/services/providers/models/base.py +145 -0
  80. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  81. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  82. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  83. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  84. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  85. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  86. django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
  87. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  88. django_cfg/apps/payments/services/providers/registry.py +9 -37
  89. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  90. django_cfg/apps/payments/services/types/requests.py +19 -7
  91. django_cfg/apps/payments/signals/payment_signals.py +31 -2
  92. django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
  93. django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
  94. django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
  95. django_cfg/apps/payments/tasks/__init__.py +39 -0
  96. django_cfg/apps/payments/tasks/types.py +73 -0
  97. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  98. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  99. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  100. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  101. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  102. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  103. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  104. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  105. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  106. django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
  107. django_cfg/apps/payments/urls.py +3 -2
  108. django_cfg/apps/payments/urls_admin.py +1 -1
  109. django_cfg/apps/payments/views/api/currencies.py +8 -5
  110. django_cfg/apps/payments/views/overview/services.py +2 -2
  111. django_cfg/apps/payments/views/serializers/currencies.py +22 -8
  112. django_cfg/apps/support/admin/__init__.py +10 -1
  113. django_cfg/apps/support/admin/support_admin.py +338 -141
  114. django_cfg/apps/tasks/admin/__init__.py +11 -0
  115. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  116. django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
  117. django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
  118. django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
  119. django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
  120. django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
  121. django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
  122. django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
  123. django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
  124. django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
  125. django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
  126. django_cfg/apps/tasks/tasks/__init__.py +10 -0
  127. django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
  128. django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
  129. django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
  130. django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
  131. django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
  132. django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
  133. django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
  134. django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
  135. django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
  136. django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
  137. django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
  138. django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
  139. django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
  140. django_cfg/apps/tasks/urls.py +2 -2
  141. django_cfg/apps/tasks/urls_admin.py +2 -2
  142. django_cfg/apps/tasks/utils/__init__.py +1 -0
  143. django_cfg/apps/tasks/utils/simulator.py +356 -0
  144. django_cfg/apps/tasks/views/__init__.py +16 -0
  145. django_cfg/apps/tasks/views/api.py +569 -0
  146. django_cfg/apps/tasks/views/dashboard.py +58 -0
  147. django_cfg/config.py +1 -1
  148. django_cfg/core/config.py +10 -5
  149. django_cfg/core/generation.py +1 -1
  150. django_cfg/core/integration/__init__.py +21 -0
  151. django_cfg/management/commands/__init__.py +13 -1
  152. django_cfg/management/commands/migrate_all.py +9 -3
  153. django_cfg/management/commands/migrator.py +11 -6
  154. django_cfg/management/commands/rundramatiq.py +3 -2
  155. django_cfg/management/commands/rundramatiq_simulator.py +430 -0
  156. django_cfg/middleware/__init__.py +0 -2
  157. django_cfg/models/api_keys.py +115 -0
  158. django_cfg/models/constance.py +0 -11
  159. django_cfg/models/payments.py +137 -3
  160. django_cfg/modules/django_admin/__init__.py +64 -0
  161. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  162. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  163. django_cfg/modules/django_admin/decorators/display.py +106 -0
  164. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  165. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  166. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  167. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  168. django_cfg/modules/django_admin/models/__init__.py +20 -0
  169. django_cfg/modules/django_admin/models/action_models.py +33 -0
  170. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  171. django_cfg/modules/django_admin/models/base.py +26 -0
  172. django_cfg/modules/django_admin/models/display_models.py +31 -0
  173. django_cfg/modules/django_admin/utils/badges.py +159 -0
  174. django_cfg/modules/django_admin/utils/displays.py +247 -0
  175. django_cfg/modules/django_currency/__init__.py +2 -2
  176. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  177. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  178. django_cfg/modules/django_currency/core/converter.py +12 -12
  179. django_cfg/modules/django_currency/database/__init__.py +2 -2
  180. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  181. django_cfg/modules/django_llm/llm/client.py +10 -2
  182. django_cfg/modules/django_tasks.py +54 -21
  183. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  184. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  185. django_cfg/modules/django_unfold/dashboard.py +14 -13
  186. django_cfg/modules/django_unfold/models/config.py +1 -1
  187. django_cfg/registry/core.py +7 -9
  188. django_cfg/registry/third_party.py +2 -2
  189. django_cfg/template_archive/django_sample.zip +0 -0
  190. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -1
  191. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/RECORD +198 -160
  192. django_cfg/apps/accounts/admin/activity.py +0 -96
  193. django_cfg/apps/accounts/admin/group.py +0 -17
  194. django_cfg/apps/accounts/admin/otp.py +0 -59
  195. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  196. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  197. django_cfg/apps/accounts/admin/user.py +0 -300
  198. django_cfg/apps/agents/core/agent.py +0 -281
  199. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  200. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  201. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  202. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  203. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  204. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  205. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  206. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  207. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  208. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  209. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  210. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  211. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  212. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  213. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  214. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  215. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  216. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  217. django_cfg/apps/payments/config/constance/__init__.py +0 -22
  218. django_cfg/apps/payments/config/constance/config_service.py +0 -123
  219. django_cfg/apps/payments/config/constance/fields.py +0 -69
  220. django_cfg/apps/payments/config/constance/settings.py +0 -160
  221. django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
  222. django_cfg/apps/tasks/admin.py +0 -320
  223. django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
  224. django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
  225. django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
  226. django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
  227. django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
  228. django_cfg/apps/tasks/templates/tasks/base.html +0 -96
  229. django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
  230. django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
  231. django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
  232. django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
  233. django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
  234. django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
  235. django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
  236. django_cfg/apps/tasks/views.py +0 -461
  237. django_cfg/management/commands/auto_generate.py +0 -486
  238. django_cfg/middleware/static_nocache.py +0 -55
  239. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  240. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  241. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  242. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  243. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  244. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -1,41 +1,51 @@
1
1
  """
2
- Admin interface for ScheduledMaintenance with Unfold styling.
2
+ ScheduledMaintenance admin using Django Admin Utilities.
3
3
 
4
- Provides comprehensive management of scheduled maintenance events.
4
+ Enhanced scheduled maintenance management with Material Icons and optimized queries.
5
5
  """
6
6
 
7
- from django.contrib import admin
8
- from django.utils.html import format_html
7
+ from django.contrib import admin, messages
9
8
  from django.http import HttpRequest, HttpResponse
10
9
  from django.shortcuts import redirect
11
- from django.contrib import messages
12
10
  from django.utils import timezone
13
11
  from django.urls import path, reverse
14
12
  from django.template.response import TemplateResponse
13
+ from django.db import models
14
+ from django.db.models import Count, Q
15
15
  from typing import Any
16
-
17
16
  from unfold.admin import ModelAdmin
18
- from unfold.decorators import display, action
19
- from unfold.enums import ActionVariant
17
+
18
+ from django_cfg.modules.django_admin import (
19
+ OptimizedModelAdmin,
20
+ DisplayMixin,
21
+ StatusBadgeConfig,
22
+ DateTimeDisplayConfig,
23
+ Icons,
24
+ ActionVariant,
25
+ display,
26
+ action
27
+ )
28
+ from django_cfg.modules.django_admin.utils.badges import StatusBadge
20
29
 
21
30
  from ..models import ScheduledMaintenance, CloudflareSite
22
31
 
23
32
 
24
33
  @admin.register(ScheduledMaintenance)
25
- class ScheduledMaintenanceAdmin(ModelAdmin):
26
- """Admin for ScheduledMaintenance with Unfold styling."""
34
+ class ScheduledMaintenanceAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin):
35
+ """Admin for ScheduledMaintenance using Django Admin Utilities."""
27
36
 
28
37
  list_display = [
29
38
  "status_display",
30
- "title",
31
- "scheduled_start",
39
+ "title_display",
40
+ "scheduled_start_display",
32
41
  "duration_display",
33
42
  "sites_count",
34
- "priority_badge",
35
- "auto_flags",
36
- "created_at",
43
+ "priority_display",
44
+ "auto_flags_display",
45
+ "created_at_display",
37
46
  ]
38
- list_display_links = ["title"]
47
+ list_display_links = ["title_display"]
48
+ ordering = ["-scheduled_start"]
39
49
  search_fields = ["title", "description", "maintenance_message"]
40
50
  list_filter = [
41
51
  "status",
@@ -45,346 +55,247 @@ class ScheduledMaintenanceAdmin(ModelAdmin):
45
55
  "scheduled_start",
46
56
  "created_at"
47
57
  ]
48
- ordering = ["-scheduled_start"]
49
58
 
50
59
  fieldsets = [
51
- ("Basic Information", {
52
- 'fields': ['title', 'description', 'priority', 'created_by']
60
+ ("📋 Basic Information", {
61
+ "fields": ["title", "description"],
62
+ "classes": ("tab",)
53
63
  }),
54
- ("Scheduling", {
55
- 'fields': ['scheduled_start', 'estimated_duration', 'scheduled_end']
64
+ ("Scheduling", {
65
+ "fields": ["scheduled_start", "duration_minutes"],
66
+ "classes": ("tab",)
56
67
  }),
57
- ("Sites", {
58
- 'fields': ['sites']
68
+ ("🌐 Sites", {
69
+ "fields": ["sites"],
70
+ "classes": ("tab",)
59
71
  }),
60
- ("Configuration", {
61
- 'fields': ['maintenance_message', 'template', 'auto_enable', 'auto_disable']
72
+ ("⚙️ Settings", {
73
+ "fields": ["priority", "auto_enable", "auto_disable"],
74
+ "classes": ("tab",)
62
75
  }),
63
- ("Notifications", {
64
- 'fields': ['notify_before', 'notify_on_start', 'notify_on_complete'],
65
- 'classes': ['collapse']
76
+ ("💬 Messages", {
77
+ "fields": ["maintenance_message"],
78
+ "classes": ("tab", "collapse")
66
79
  }),
67
- ("Execution Status", {
68
- 'fields': ['status', 'actual_start', 'actual_end'],
69
- 'classes': ['collapse']
80
+ ("📊 Status", {
81
+ "fields": ["status"],
82
+ "classes": ("tab",)
70
83
  }),
71
- ("Execution Log", {
72
- 'fields': ['execution_log'],
73
- 'classes': ['collapse']
84
+ (" Timestamps", {
85
+ "fields": ["created_at", "updated_at"],
86
+ "classes": ("tab", "collapse")
74
87
  }),
75
88
  ]
76
89
 
77
- readonly_fields = ['scheduled_end', 'actual_start', 'actual_end', 'execution_log']
78
-
79
- filter_horizontal = ['sites']
90
+ filter_horizontal = ["sites"]
80
91
 
81
92
  actions = [
82
- 'start_maintenance_action',
83
- 'complete_maintenance_action',
84
- 'cancel_maintenance_action',
85
- 'duplicate_maintenance_action'
93
+ "execute_maintenance_action",
94
+ "cancel_maintenance_action",
95
+ "reschedule_maintenance_action"
86
96
  ]
87
97
 
88
- def get_urls(self):
89
- """Add custom admin URLs."""
90
- urls = super().get_urls()
91
- custom_urls = [
92
- path(
93
- 'calendar/',
94
- self.admin_site.admin_view(self.calendar_view),
95
- name='scheduled_maintenance_calendar'
96
- ),
97
- path(
98
- '<int:object_id>/start/',
99
- self.admin_site.admin_view(self.start_maintenance_view),
100
- name='scheduled_maintenance_start'
101
- ),
102
- path(
103
- '<int:object_id>/complete/',
104
- self.admin_site.admin_view(self.complete_maintenance_view),
105
- name='scheduled_maintenance_complete'
106
- ),
107
- ]
108
- return custom_urls + urls
109
-
110
98
  @display(description="Status")
111
99
  def status_display(self, obj: ScheduledMaintenance) -> str:
112
- """Display status with colored badge and timing info."""
113
- status_config = {
114
- ScheduledMaintenance.Status.SCHEDULED: {
115
- 'emoji': '📅',
116
- 'color': 'blue',
117
- 'text': 'Scheduled'
118
- },
119
- ScheduledMaintenance.Status.ACTIVE: {
120
- 'emoji': '🔧',
121
- 'color': 'orange',
122
- 'text': 'Active'
123
- },
124
- ScheduledMaintenance.Status.COMPLETED: {
125
- 'emoji': '✅',
126
- 'color': 'green',
127
- 'text': 'Completed'
128
- },
129
- ScheduledMaintenance.Status.CANCELLED: {
130
- 'emoji': '❌',
131
- 'color': 'red',
132
- 'text': 'Cancelled'
133
- },
134
- ScheduledMaintenance.Status.FAILED: {
135
- 'emoji': '💥',
136
- 'color': 'red',
137
- 'text': 'Failed'
138
- },
100
+ """Display status with badge."""
101
+ status_variants = {
102
+ ScheduledMaintenance.Status.SCHEDULED: 'warning',
103
+ ScheduledMaintenance.Status.ACTIVE: 'info',
104
+ ScheduledMaintenance.Status.COMPLETED: 'success',
105
+ ScheduledMaintenance.Status.CANCELLED: 'secondary',
106
+ ScheduledMaintenance.Status.FAILED: 'danger'
139
107
  }
108
+ variant = status_variants.get(obj.status, 'secondary')
140
109
 
141
- config = status_config.get(obj.status, {
142
- 'emoji': '❓',
143
- 'color': 'gray',
144
- 'text': obj.get_status_display()
145
- })
146
-
147
- # Add timing info
148
- timing_info = ""
149
- if obj.status == ScheduledMaintenance.Status.SCHEDULED:
150
- if obj.is_due:
151
- timing_info = " <small>(Due now!)</small>"
152
- elif obj.time_until_start:
153
- hours = int(obj.time_until_start.total_seconds() // 3600)
154
- if hours < 24:
155
- timing_info = f" <small>(in {hours}h)</small>"
156
- elif obj.status == ScheduledMaintenance.Status.ACTIVE:
157
- if obj.is_overdue:
158
- timing_info = " <small>(Overdue!)</small>"
159
- elif obj.time_until_end:
160
- hours = int(obj.time_until_end.total_seconds() // 3600)
161
- minutes = int((obj.time_until_end.total_seconds() % 3600) // 60)
162
- timing_info = f" <small>({hours}h {minutes}m left)</small>"
110
+ status_icons = {
111
+ ScheduledMaintenance.Status.SCHEDULED: Icons.SCHEDULE,
112
+ ScheduledMaintenance.Status.ACTIVE: Icons.PLAY_ARROW,
113
+ ScheduledMaintenance.Status.COMPLETED: Icons.CHECK_CIRCLE,
114
+ ScheduledMaintenance.Status.CANCELLED: Icons.CANCEL,
115
+ ScheduledMaintenance.Status.FAILED: Icons.ERROR
116
+ }
117
+ icon = status_icons.get(obj.status, Icons.HELP)
163
118
 
164
- return format_html(
165
- '<span style="color: {};">{} {}</span>{}',
166
- config['color'],
167
- config['emoji'],
168
- config['text'],
169
- timing_info
119
+ config = StatusBadgeConfig(show_icons=True, icon=icon)
120
+ return StatusBadge.create(
121
+ text=obj.get_status_display(),
122
+ variant=variant,
123
+ config=config
170
124
  )
171
125
 
126
+ @display(description="Title", ordering="title")
127
+ def title_display(self, obj: ScheduledMaintenance) -> str:
128
+ """Display maintenance title."""
129
+ config = StatusBadgeConfig(show_icons=True, icon=Icons.EVENT)
130
+ return StatusBadge.create(
131
+ text=obj.title,
132
+ variant="primary",
133
+ config=config
134
+ )
135
+
136
+ @display(description="Scheduled Start")
137
+ def scheduled_start_display(self, obj: ScheduledMaintenance) -> str:
138
+ """Display scheduled start time."""
139
+ config = DateTimeDisplayConfig(show_relative=True)
140
+ return self.display_datetime_relative(obj, 'scheduled_start', config)
141
+
172
142
  @display(description="Duration")
173
143
  def duration_display(self, obj: ScheduledMaintenance) -> str:
174
- """Display estimated vs actual duration."""
175
- estimated_hours = obj.estimated_duration.total_seconds() / 3600
176
-
177
- if obj.actual_duration:
178
- actual_hours = obj.actual_duration.total_seconds() / 3600
179
- return format_html(
180
- '{:.1f}h <small>(actual: {:.1f}h)</small>',
181
- estimated_hours,
182
- actual_hours
183
- )
184
-
185
- return f"{estimated_hours:.1f}h"
144
+ """Display maintenance duration."""
145
+ if obj.duration_minutes < 60:
146
+ return f"{obj.duration_minutes} min"
147
+ else:
148
+ hours = obj.duration_minutes // 60
149
+ minutes = obj.duration_minutes % 60
150
+ if minutes == 0:
151
+ return f"{hours}h"
152
+ else:
153
+ return f"{hours}h {minutes}m"
186
154
 
187
155
  @display(description="Sites")
188
156
  def sites_count(self, obj: ScheduledMaintenance) -> str:
189
- """Display sites count with link."""
190
- count = obj.affected_sites_count
157
+ """Display count of affected sites."""
158
+ count = obj.sites.count()
191
159
  if count == 0:
192
- return format_html('<span style="color: red;">No sites</span>')
193
-
194
- return format_html(
195
- '<span class="badge badge-info">{} sites</span>',
196
- count
197
- )
160
+ return "No sites"
161
+ elif count == 1:
162
+ return "1 site"
163
+ else:
164
+ return f"{count} sites"
198
165
 
199
166
  @display(description="Priority")
200
- def priority_badge(self, obj: ScheduledMaintenance) -> str:
201
- """Display priority badge."""
202
- priority_config = {
203
- 'low': {'color': 'green', 'emoji': '🟢'},
204
- 'normal': {'color': 'blue', 'emoji': '🟡'},
205
- 'high': {'color': 'orange', 'emoji': '🟠'},
206
- 'critical': {'color': 'red', 'emoji': '🔴'},
167
+ def priority_display(self, obj: ScheduledMaintenance) -> str:
168
+ """Display priority with badge."""
169
+ priority_variants = {
170
+ ScheduledMaintenance.Priority.LOW: 'secondary',
171
+ ScheduledMaintenance.Priority.MEDIUM: 'info',
172
+ ScheduledMaintenance.Priority.HIGH: 'warning',
173
+ ScheduledMaintenance.Priority.CRITICAL: 'danger'
207
174
  }
175
+ variant = priority_variants.get(obj.priority, 'secondary')
208
176
 
209
- config = priority_config.get(obj.priority, {'color': 'gray', 'emoji': '⚪'})
177
+ priority_icons = {
178
+ ScheduledMaintenance.Priority.LOW: Icons.KEYBOARD_ARROW_DOWN,
179
+ ScheduledMaintenance.Priority.MEDIUM: Icons.REMOVE,
180
+ ScheduledMaintenance.Priority.HIGH: Icons.KEYBOARD_ARROW_UP,
181
+ ScheduledMaintenance.Priority.CRITICAL: Icons.PRIORITY_HIGH
182
+ }
183
+ icon = priority_icons.get(obj.priority, Icons.HELP)
210
184
 
211
- return format_html(
212
- '<span style="color: {};">{} {}</span>',
213
- config['color'],
214
- config['emoji'],
215
- obj.get_priority_display()
185
+ config = StatusBadgeConfig(show_icons=True, icon=icon)
186
+ return StatusBadge.create(
187
+ text=obj.get_priority_display(),
188
+ variant=variant,
189
+ config=config
216
190
  )
217
191
 
218
- @display(description="Auto")
219
- def auto_flags(self, obj: ScheduledMaintenance) -> str:
220
- """Display automation flags."""
192
+ @display(description="Auto Flags")
193
+ def auto_flags_display(self, obj: ScheduledMaintenance) -> str:
194
+ """Display auto enable/disable flags."""
221
195
  flags = []
222
-
223
196
  if obj.auto_enable:
224
- flags.append('<span style="color: green;">▶️ Start</span>')
225
-
197
+ flags.append("Auto Enable")
226
198
  if obj.auto_disable:
227
- flags.append('<span style="color: blue;">⏹️ Stop</span>')
199
+ flags.append("Auto Disable")
228
200
 
229
201
  if not flags:
230
- return '<span style="color: gray;">Manual</span>'
202
+ return "Manual"
231
203
 
232
- return format_html(' '.join(flags))
204
+ return " | ".join(flags)
233
205
 
234
- @action(description="Start Maintenance", variant=ActionVariant.SUCCESS)
235
- def start_maintenance_action(self, request: HttpRequest, queryset: Any) -> None:
236
- """Start selected maintenance events."""
237
- started = 0
238
- failed = 0
239
-
240
- for maintenance in queryset:
241
- if maintenance.status == ScheduledMaintenance.Status.SCHEDULED:
242
- try:
243
- result = maintenance.start_maintenance()
244
- if result['success']:
245
- started += 1
246
- else:
247
- failed += 1
248
- except Exception as e:
249
- failed += 1
250
- messages.error(request, f"Failed to start {maintenance.title}: {e}")
251
-
252
- if started > 0:
253
- messages.success(request, f"Started {started} maintenance events")
254
- if failed > 0:
255
- messages.error(request, f"Failed to start {failed} maintenance events")
206
+ @display(description="Created")
207
+ def created_at_display(self, obj: ScheduledMaintenance) -> str:
208
+ """Created time with relative display."""
209
+ config = DateTimeDisplayConfig(show_relative=True)
210
+ return self.display_datetime_relative(obj, 'created_at', config)
256
211
 
257
- @action(description="Complete Maintenance", variant=ActionVariant.PRIMARY)
258
- def complete_maintenance_action(self, request: HttpRequest, queryset: Any) -> None:
259
- """Complete selected maintenance events."""
260
- completed = 0
261
- failed = 0
212
+ @action(description="Execute maintenance", variant=ActionVariant.WARNING)
213
+ def execute_maintenance_action(self, request: HttpRequest, queryset) -> None:
214
+ """Execute selected maintenance tasks."""
215
+ scheduled_count = queryset.filter(status=ScheduledMaintenance.Status.SCHEDULED).count()
216
+ if scheduled_count == 0:
217
+ messages.error(request, "No scheduled maintenance tasks selected.")
218
+ return
262
219
 
263
- for maintenance in queryset:
264
- if maintenance.status == ScheduledMaintenance.Status.ACTIVE:
265
- try:
266
- result = maintenance.complete_maintenance()
267
- if result['success']:
268
- completed += 1
269
- else:
270
- failed += 1
271
- except Exception as e:
272
- failed += 1
273
- messages.error(request, f"Failed to complete {maintenance.title}: {e}")
220
+ # Update status to active
221
+ queryset.filter(status=ScheduledMaintenance.Status.SCHEDULED).update(
222
+ status=ScheduledMaintenance.Status.ACTIVE
223
+ )
274
224
 
275
- if completed > 0:
276
- messages.success(request, f"Completed {completed} maintenance events")
277
- if failed > 0:
278
- messages.error(request, f"Failed to complete {failed} maintenance events")
225
+ messages.success(request, f"Started execution of {scheduled_count} maintenance tasks.")
279
226
 
280
- @action(description="Cancel Maintenance", variant=ActionVariant.DANGER)
281
- def cancel_maintenance_action(self, request: HttpRequest, queryset: Any) -> None:
282
- """Cancel selected maintenance events."""
283
- cancelled = 0
284
-
285
- for maintenance in queryset:
286
- if maintenance.status in [ScheduledMaintenance.Status.SCHEDULED, ScheduledMaintenance.Status.ACTIVE]:
287
- try:
288
- result = maintenance.cancel_maintenance(reason="Cancelled via admin")
289
- if result['success']:
290
- cancelled += 1
291
- except Exception as e:
292
- messages.error(request, f"Failed to cancel {maintenance.title}: {e}")
227
+ @action(description="Cancel maintenance", variant=ActionVariant.DANGER)
228
+ def cancel_maintenance_action(self, request: HttpRequest, queryset) -> None:
229
+ """Cancel selected maintenance tasks."""
230
+ cancelable_count = queryset.filter(
231
+ status__in=[ScheduledMaintenance.Status.SCHEDULED, ScheduledMaintenance.Status.ACTIVE]
232
+ ).count()
293
233
 
294
- if cancelled > 0:
295
- messages.success(request, f"Cancelled {cancelled} maintenance events")
296
-
297
- @action(description="Duplicate Maintenance")
298
- def duplicate_maintenance_action(self, request: HttpRequest, queryset: Any) -> None:
299
- """Duplicate selected maintenance events."""
300
- duplicated = 0
234
+ if cancelable_count == 0:
235
+ messages.error(request, "No cancelable maintenance tasks selected.")
236
+ return
301
237
 
302
- for maintenance in queryset:
303
- try:
304
- # Create duplicate with new start time (1 week later)
305
- new_start = maintenance.scheduled_start + timezone.timedelta(weeks=1)
306
-
307
- duplicate = ScheduledMaintenance.objects.create(
308
- title=f"{maintenance.title} (Copy)",
309
- description=maintenance.description,
310
- scheduled_start=new_start,
311
- estimated_duration=maintenance.estimated_duration,
312
- maintenance_message=maintenance.maintenance_message,
313
- template=maintenance.template,
314
- priority=maintenance.priority,
315
- auto_enable=maintenance.auto_enable,
316
- auto_disable=maintenance.auto_disable,
317
- notify_before=maintenance.notify_before,
318
- created_by=f"{maintenance.created_by} (duplicate)"
319
- )
320
-
321
- # Copy sites
322
- duplicate.sites.set(maintenance.sites.all())
323
- duplicated += 1
324
-
325
- except Exception as e:
326
- messages.error(request, f"Failed to duplicate {maintenance.title}: {e}")
238
+ queryset.filter(
239
+ status__in=[ScheduledMaintenance.Status.SCHEDULED, ScheduledMaintenance.Status.ACTIVE]
240
+ ).update(status=ScheduledMaintenance.Status.CANCELLED)
327
241
 
328
- if duplicated > 0:
329
- messages.success(request, f"Duplicated {duplicated} maintenance events")
242
+ messages.warning(request, f"Cancelled {cancelable_count} maintenance tasks.")
330
243
 
331
- def calendar_view(self, request: HttpRequest) -> TemplateResponse:
332
- """Calendar view for scheduled maintenances."""
333
- from ..services.scheduled_maintenance_service import scheduled_maintenance_service
244
+ @action(description="Reschedule maintenance", variant=ActionVariant.INFO)
245
+ def reschedule_maintenance_action(self, request: HttpRequest, queryset) -> None:
246
+ """Reschedule selected maintenance tasks."""
247
+ reschedulable_count = queryset.filter(
248
+ status__in=[ScheduledMaintenance.Status.CANCELLED, ScheduledMaintenance.Status.FAILED]
249
+ ).count()
334
250
 
335
- calendar_data = scheduled_maintenance_service.get_maintenance_calendar(days=30)
251
+ if reschedulable_count == 0:
252
+ messages.error(request, "No reschedulable maintenance tasks selected.")
253
+ return
336
254
 
337
- context = {
338
- 'title': 'Maintenance Calendar',
339
- 'calendar_data': calendar_data,
340
- 'opts': self.model._meta,
341
- }
255
+ # Reset to scheduled status
256
+ queryset.filter(
257
+ status__in=[ScheduledMaintenance.Status.CANCELLED, ScheduledMaintenance.Status.FAILED]
258
+ ).update(status=ScheduledMaintenance.Status.SCHEDULED)
342
259
 
343
- return TemplateResponse(
344
- request,
345
- 'admin/maintenance/scheduled_maintenance_calendar.html',
346
- context
347
- )
260
+ messages.info(request, f"Reset {reschedulable_count} maintenance tasks to scheduled.")
348
261
 
349
- def start_maintenance_view(self, request: HttpRequest, object_id: int) -> HttpResponse:
350
- """Start specific maintenance event."""
351
- maintenance = self.get_object(request, object_id)
262
+ def changelist_view(self, request, extra_context=None):
263
+ """Add maintenance statistics to changelist."""
264
+ extra_context = extra_context or {}
352
265
 
353
- if maintenance.status != ScheduledMaintenance.Status.SCHEDULED:
354
- messages.error(request, f"Cannot start maintenance in {maintenance.status} status")
355
- else:
356
- try:
357
- result = maintenance.start_maintenance()
358
- if result['success']:
359
- messages.success(
360
- request,
361
- f"Started maintenance '{maintenance.title}' affecting {result['sites_affected']} sites"
362
- )
363
- else:
364
- messages.error(request, f"Failed to start maintenance: {result.get('error')}")
365
- except Exception as e:
366
- messages.error(request, f"Error starting maintenance: {e}")
266
+ queryset = self.get_queryset(request)
267
+ stats = queryset.aggregate(
268
+ total_maintenance=Count('id'),
269
+ scheduled_maintenance=Count('id', filter=Q(status=ScheduledMaintenance.Status.SCHEDULED)),
270
+ active_maintenance=Count('id', filter=Q(status=ScheduledMaintenance.Status.ACTIVE)),
271
+ completed_maintenance=Count('id', filter=Q(status=ScheduledMaintenance.Status.COMPLETED)),
272
+ failed_maintenance=Count('id', filter=Q(status=ScheduledMaintenance.Status.FAILED)),
273
+ cancelled_maintenance=Count('id', filter=Q(status=ScheduledMaintenance.Status.CANCELLED))
274
+ )
367
275
 
368
- return redirect('admin:maintenance_scheduledmaintenance_change', object_id)
369
-
370
- def complete_maintenance_view(self, request: HttpRequest, object_id: int) -> HttpResponse:
371
- """Complete specific maintenance event."""
372
- maintenance = self.get_object(request, object_id)
276
+ # Priority breakdown
277
+ priority_counts = dict(
278
+ queryset.values_list('priority').annotate(
279
+ count=Count('id')
280
+ )
281
+ )
373
282
 
374
- if maintenance.status != ScheduledMaintenance.Status.ACTIVE:
375
- messages.error(request, f"Cannot complete maintenance in {maintenance.status} status")
376
- else:
377
- try:
378
- result = maintenance.complete_maintenance()
379
- if result['success']:
380
- duration = result.get('actual_duration', 0) / 3600
381
- messages.success(
382
- request,
383
- f"Completed maintenance '{maintenance.title}' (duration: {duration:.1f}h)"
384
- )
385
- else:
386
- messages.error(request, f"Failed to complete maintenance: {result.get('error')}")
387
- except Exception as e:
388
- messages.error(request, f"Error completing maintenance: {e}")
283
+ # Upcoming maintenance (next 7 days)
284
+ upcoming_maintenance = queryset.filter(
285
+ scheduled_start__gte=timezone.now(),
286
+ scheduled_start__lte=timezone.now() + timezone.timedelta(days=7),
287
+ status=ScheduledMaintenance.Status.SCHEDULED
288
+ ).count()
289
+
290
+ extra_context['maintenance_stats'] = {
291
+ 'total_maintenance': stats['total_maintenance'] or 0,
292
+ 'scheduled_maintenance': stats['scheduled_maintenance'] or 0,
293
+ 'active_maintenance': stats['active_maintenance'] or 0,
294
+ 'completed_maintenance': stats['completed_maintenance'] or 0,
295
+ 'failed_maintenance': stats['failed_maintenance'] or 0,
296
+ 'cancelled_maintenance': stats['cancelled_maintenance'] or 0,
297
+ 'priority_counts': priority_counts,
298
+ 'upcoming_maintenance': upcoming_maintenance
299
+ }
389
300
 
390
- return redirect('admin:maintenance_scheduledmaintenance_change', object_id)
301
+ return super().changelist_view(request, extra_context)