django-cfg 1.2.17__py3-none-any.whl → 1.2.18__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 (81) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/models/__init__.py +68 -0
  3. django_cfg/apps/accounts/models/activity.py +34 -0
  4. django_cfg/apps/accounts/models/auth.py +50 -0
  5. django_cfg/apps/accounts/models/base.py +8 -0
  6. django_cfg/apps/accounts/models/choices.py +32 -0
  7. django_cfg/apps/accounts/models/integrations.py +75 -0
  8. django_cfg/apps/accounts/models/registration.py +52 -0
  9. django_cfg/apps/accounts/models/user.py +80 -0
  10. django_cfg/apps/maintenance/__init__.py +53 -24
  11. django_cfg/apps/maintenance/admin/__init__.py +7 -18
  12. django_cfg/apps/maintenance/admin/api_key_admin.py +185 -0
  13. django_cfg/apps/maintenance/admin/log_admin.py +156 -0
  14. django_cfg/apps/maintenance/admin/scheduled_admin.py +390 -0
  15. django_cfg/apps/maintenance/admin/site_admin.py +448 -0
  16. django_cfg/apps/maintenance/apps.py +9 -96
  17. django_cfg/apps/maintenance/management/commands/maintenance.py +193 -307
  18. django_cfg/apps/maintenance/management/commands/process_scheduled_maintenance.py +241 -0
  19. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +152 -111
  20. django_cfg/apps/maintenance/managers/__init__.py +7 -12
  21. django_cfg/apps/maintenance/managers/cloudflare_site_manager.py +192 -0
  22. django_cfg/apps/maintenance/managers/maintenance_log_manager.py +151 -0
  23. django_cfg/apps/maintenance/migrations/0001_initial.py +145 -705
  24. django_cfg/apps/maintenance/migrations/0002_cloudflaresite_maintenance_url.py +21 -0
  25. django_cfg/apps/maintenance/models/__init__.py +23 -21
  26. django_cfg/apps/maintenance/models/cloudflare_api_key.py +109 -0
  27. django_cfg/apps/maintenance/models/cloudflare_site.py +125 -0
  28. django_cfg/apps/maintenance/models/maintenance_log.py +131 -0
  29. django_cfg/apps/maintenance/models/scheduled_maintenance.py +307 -0
  30. django_cfg/apps/maintenance/services/__init__.py +37 -16
  31. django_cfg/apps/maintenance/services/bulk_operations_service.py +400 -0
  32. django_cfg/apps/maintenance/services/maintenance_service.py +230 -0
  33. django_cfg/apps/maintenance/services/scheduled_maintenance_service.py +381 -0
  34. django_cfg/apps/maintenance/services/site_sync_service.py +390 -0
  35. django_cfg/apps/maintenance/utils/__init__.py +12 -0
  36. django_cfg/apps/maintenance/utils/retry_utils.py +109 -0
  37. django_cfg/config.py +3 -0
  38. django_cfg/core/config.py +4 -6
  39. django_cfg/modules/django_unfold/dashboard.py +4 -5
  40. {django_cfg-1.2.17.dist-info → django_cfg-1.2.18.dist-info}/METADATA +52 -1
  41. {django_cfg-1.2.17.dist-info → django_cfg-1.2.18.dist-info}/RECORD +45 -55
  42. django_cfg/apps/maintenance/README.md +0 -305
  43. django_cfg/apps/maintenance/admin/deployments_admin.py +0 -251
  44. django_cfg/apps/maintenance/admin/events_admin.py +0 -374
  45. django_cfg/apps/maintenance/admin/monitoring_admin.py +0 -215
  46. django_cfg/apps/maintenance/admin/sites_admin.py +0 -464
  47. django_cfg/apps/maintenance/managers/deployments.py +0 -287
  48. django_cfg/apps/maintenance/managers/events.py +0 -374
  49. django_cfg/apps/maintenance/managers/monitoring.py +0 -301
  50. django_cfg/apps/maintenance/managers/sites.py +0 -335
  51. django_cfg/apps/maintenance/models/cloudflare.py +0 -316
  52. django_cfg/apps/maintenance/models/maintenance.py +0 -334
  53. django_cfg/apps/maintenance/models/monitoring.py +0 -393
  54. django_cfg/apps/maintenance/models/sites.py +0 -419
  55. django_cfg/apps/maintenance/serializers/__init__.py +0 -60
  56. django_cfg/apps/maintenance/serializers/actions.py +0 -310
  57. django_cfg/apps/maintenance/serializers/base.py +0 -44
  58. django_cfg/apps/maintenance/serializers/deployments.py +0 -209
  59. django_cfg/apps/maintenance/serializers/events.py +0 -210
  60. django_cfg/apps/maintenance/serializers/monitoring.py +0 -278
  61. django_cfg/apps/maintenance/serializers/sites.py +0 -213
  62. django_cfg/apps/maintenance/services/README.md +0 -168
  63. django_cfg/apps/maintenance/services/cloudflare_client.py +0 -441
  64. django_cfg/apps/maintenance/services/dns_manager.py +0 -497
  65. django_cfg/apps/maintenance/services/maintenance_manager.py +0 -504
  66. django_cfg/apps/maintenance/services/site_sync.py +0 -448
  67. django_cfg/apps/maintenance/services/sync_command_service.py +0 -330
  68. django_cfg/apps/maintenance/services/worker_manager.py +0 -264
  69. django_cfg/apps/maintenance/signals.py +0 -38
  70. django_cfg/apps/maintenance/urls.py +0 -36
  71. django_cfg/apps/maintenance/views/__init__.py +0 -18
  72. django_cfg/apps/maintenance/views/base.py +0 -61
  73. django_cfg/apps/maintenance/views/deployments.py +0 -175
  74. django_cfg/apps/maintenance/views/events.py +0 -204
  75. django_cfg/apps/maintenance/views/monitoring.py +0 -213
  76. django_cfg/apps/maintenance/views/sites.py +0 -338
  77. django_cfg/models/cloudflare.py +0 -316
  78. /django_cfg/apps/accounts/{models.py → __models.py} +0 -0
  79. {django_cfg-1.2.17.dist-info → django_cfg-1.2.18.dist-info}/WHEEL +0 -0
  80. {django_cfg-1.2.17.dist-info → django_cfg-1.2.18.dist-info}/entry_points.txt +0 -0
  81. {django_cfg-1.2.17.dist-info → django_cfg-1.2.18.dist-info}/licenses/LICENSE +0 -0
@@ -1,251 +0,0 @@
1
- """
2
- Deployments admin interfaces with Unfold optimization.
3
- """
4
-
5
- from django.contrib import admin
6
- from django.utils.html import format_html
7
- from django.urls import reverse
8
- from django.utils.safestring import mark_safe
9
- from django.db import models
10
- from django.db.models import Count, Q
11
- from django.contrib import messages
12
- from django.shortcuts import redirect
13
- from unfold.admin import ModelAdmin, TabularInline
14
- from unfold.decorators import display, action
15
- from unfold.enums import ActionVariant
16
- from unfold.contrib.filters.admin import AutocompleteSelectFilter
17
- from django_cfg import ImportExportModelAdmin, ExportMixin
18
-
19
- from ..models import CloudflareDeployment
20
-
21
-
22
- @admin.register(CloudflareDeployment)
23
- class CloudflareDeploymentAdmin(ModelAdmin, ImportExportModelAdmin):
24
- """Admin for CloudflareDeployment with Unfold styling."""
25
-
26
- list_display = [
27
- "deployment_with_icon",
28
- "site_link",
29
- "deployment_type_badge",
30
- "status_badge",
31
- "worker_name_display",
32
- "created_at_display",
33
- "deployed_at_display"
34
- ]
35
- list_display_links = ["deployment_with_icon"]
36
- search_fields = ["site__name", "site__domain", "worker_name", "script_name"]
37
- list_filter = [
38
- "deployment_type",
39
- "status",
40
- "created_at",
41
- "deployed_at",
42
- ("site", AutocompleteSelectFilter),
43
- ("maintenance_event", AutocompleteSelectFilter),
44
- ]
45
- ordering = ["-created_at"]
46
- readonly_fields = [
47
- "deployed_at", "created_at"
48
- ]
49
-
50
- fieldsets = (
51
- ("Deployment Information", {
52
- "fields": ("site", "deployment_type", "maintenance_event")
53
- }),
54
- ("Worker Configuration", {
55
- "fields": ("worker_name", "script_name", "script_content"),
56
- "classes": ("collapse",)
57
- }),
58
- ("Cloudflare Details", {
59
- "fields": ("worker_id", "script_id", "deployment_config"),
60
- "classes": ("collapse",)
61
- }),
62
- ("Status & Logs", {
63
- "fields": ("status", "deployed_at", "deployment_logs"),
64
- "classes": ("collapse",)
65
- }),
66
- ("Timestamps", {
67
- "fields": ("created_at", "updated_at"),
68
- "classes": ("collapse",)
69
- })
70
- )
71
-
72
- # Unfold actions
73
- actions_detail = ["deploy_worker", "undeploy_worker", "view_logs"]
74
-
75
- @display(description="Deployment", ordering="worker_name")
76
- def deployment_with_icon(self, obj):
77
- """Display deployment with status icon."""
78
- icons = {
79
- 'pending': '⏳',
80
- 'deploying': '🚀',
81
- 'deployed': '✅',
82
- 'failed': '❌',
83
- 'undeployed': '🗑️'
84
- }
85
- icon = icons.get(obj.status, '📦')
86
- name = obj.worker_name or obj.script_name or f"Deployment #{obj.id}"
87
- return format_html('{} {}', icon, name)
88
-
89
- @display(description="Site")
90
- def site_link(self, obj):
91
- """Display site with link."""
92
- return format_html(
93
- '<a href="{}" class="text-decoration-none">🌐 {}</a>',
94
- reverse('admin:django_cfg_maintenance_cloudflaresite_change',
95
- args=[obj.site.id]),
96
- obj.site.name
97
- )
98
-
99
- @display(description="Type", ordering="deployment_type")
100
- def deployment_type_badge(self, obj):
101
- """Display deployment type with colored badge."""
102
- colors = {
103
- 'maintenance_page': 'warning',
104
- 'redirect': 'info',
105
- 'custom_worker': 'primary',
106
- 'error_page': 'danger'
107
- }
108
- color = colors.get(obj.deployment_type, 'secondary')
109
- return format_html(
110
- '<span class="badge badge-{}">{}</span>',
111
- color, obj.get_deployment_type_display()
112
- )
113
-
114
- @display(description="Status", ordering="status")
115
- def status_badge(self, obj):
116
- """Display status with colored badge."""
117
- colors = {
118
- 'pending': 'secondary',
119
- 'deploying': 'info',
120
- 'deployed': 'success',
121
- 'failed': 'danger',
122
- 'undeployed': 'warning'
123
- }
124
- color = colors.get(obj.status, 'secondary')
125
- return format_html(
126
- '<span class="badge badge-{}">{}</span>',
127
- color, obj.get_status_display()
128
- )
129
-
130
- @display(description="Worker")
131
- def worker_name_display(self, obj):
132
- """Display worker name or script name."""
133
- if obj.worker_name:
134
- return obj.worker_name
135
- elif obj.script_name:
136
- return format_html('<em>{}</em>', obj.script_name)
137
- else:
138
- return format_html('<span class="text-muted">Auto-generated</span>')
139
-
140
- @display(description="Created", ordering="created_at")
141
- def created_at_display(self, obj):
142
- """Display creation time."""
143
- return obj.created_at.strftime("%Y-%m-%d %H:%M")
144
-
145
- @display(description="Deployed", ordering="deployed_at")
146
- def deployed_at_display(self, obj):
147
- """Display deployment time."""
148
- if not obj.deployed_at:
149
- return format_html('<span class="text-muted">Not deployed</span>')
150
-
151
- from django.utils import timezone
152
- from datetime import timedelta
153
-
154
- now = timezone.now()
155
- diff = now - obj.deployed_at
156
-
157
- if diff < timedelta(hours=1):
158
- color = "success"
159
- elif diff < timedelta(days=1):
160
- color = "info"
161
- else:
162
- color = "secondary"
163
-
164
- return format_html(
165
- '<span class="text-{}">{}</span>',
166
- color, obj.deployed_at.strftime("%Y-%m-%d %H:%M")
167
- )
168
-
169
- def get_queryset(self, request):
170
- """Optimize queryset."""
171
- return super().get_queryset(request).select_related(
172
- 'site', 'maintenance_event'
173
- )
174
-
175
- @action(
176
- description="🚀 Deploy Worker",
177
- icon="rocket_launch",
178
- variant=ActionVariant.SUCCESS
179
- )
180
- def deploy_worker(self, request, object_id):
181
- """Deploy Cloudflare Worker."""
182
- try:
183
- deployment = CloudflareDeployment.objects.get(id=object_id)
184
-
185
- if deployment.status == 'deployed':
186
- messages.warning(request, "Worker is already deployed")
187
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
188
-
189
- # Deploy worker logic here (placeholder)
190
- deployment.status = 'deploying'
191
- deployment.save()
192
-
193
- messages.success(
194
- request,
195
- f"Deployment started for {deployment.site.name}"
196
- )
197
-
198
- except Exception as e:
199
- messages.error(request, f"Deployment failed: {str(e)}")
200
-
201
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
202
-
203
- @action(
204
- description="🗑️ Undeploy Worker",
205
- icon="delete",
206
- variant=ActionVariant.DANGER
207
- )
208
- def undeploy_worker(self, request, object_id):
209
- """Undeploy Cloudflare Worker."""
210
- try:
211
- deployment = CloudflareDeployment.objects.get(id=object_id)
212
-
213
- if deployment.status != 'deployed':
214
- messages.warning(request, "Worker is not currently deployed")
215
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
216
-
217
- # Undeploy worker logic here (placeholder)
218
- deployment.status = 'undeployed'
219
- deployment.save()
220
-
221
- messages.success(
222
- request,
223
- f"Worker undeployed for {deployment.site.name}"
224
- )
225
-
226
- except Exception as e:
227
- messages.error(request, f"Undeployment failed: {str(e)}")
228
-
229
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
230
-
231
- @action(
232
- description="📋 View Logs",
233
- icon="description",
234
- variant=ActionVariant.INFO
235
- )
236
- def view_logs(self, request, object_id):
237
- """View deployment logs."""
238
- try:
239
- deployment = CloudflareDeployment.objects.get(id=object_id)
240
-
241
- # This would typically redirect to a logs view
242
- # For now, just show a message
243
- messages.info(
244
- request,
245
- f"Logs for deployment {deployment.id} would be displayed here"
246
- )
247
-
248
- except Exception as e:
249
- messages.error(request, f"Failed to retrieve logs: {str(e)}")
250
-
251
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
@@ -1,374 +0,0 @@
1
- """
2
- Events admin interfaces with Unfold optimization.
3
- """
4
-
5
- from django.contrib import admin
6
- from django.utils.html import format_html
7
- from django.urls import reverse
8
- from django.utils.safestring import mark_safe
9
- from django.db import models
10
- from django.db.models import Count, Q
11
- from django.contrib import messages
12
- from django.shortcuts import redirect
13
- from unfold.admin import ModelAdmin, TabularInline
14
- from unfold.decorators import display, action
15
- from unfold.enums import ActionVariant
16
- from unfold.contrib.filters.admin import AutocompleteSelectFilter, AutocompleteSelectMultipleFilter
17
- from django_cfg import ImportExportModelAdmin, ExportMixin
18
-
19
- from ..models import MaintenanceEvent, MaintenanceLog, CloudflareSite
20
-
21
-
22
- class MaintenanceLogInline(TabularInline):
23
- """Inline for maintenance logs with Unfold styling."""
24
-
25
- model = MaintenanceLog
26
- verbose_name = "Log Entry"
27
- verbose_name_plural = "📋 Maintenance Logs"
28
- extra = 0
29
- max_num = 0
30
- can_delete = False
31
- show_change_link = True
32
-
33
- def has_add_permission(self, request, obj=None):
34
- return False
35
-
36
- def has_change_permission(self, request, obj=None):
37
- return False
38
-
39
- fields = [
40
- 'timestamp_display', 'level_badge', 'component',
41
- 'operation', 'message_preview', 'user'
42
- ]
43
- readonly_fields = [
44
- 'timestamp_display', 'level_badge', 'component',
45
- 'operation', 'message_preview', 'user'
46
- ]
47
-
48
- @display(description="Time", ordering="timestamp")
49
- def timestamp_display(self, obj):
50
- """Display timestamp."""
51
- return obj.timestamp.strftime("%H:%M:%S")
52
-
53
- @display(description="Level", ordering="level")
54
- def level_badge(self, obj):
55
- """Display log level with colored badge."""
56
- colors = {
57
- 'DEBUG': 'secondary',
58
- 'INFO': 'info',
59
- 'WARNING': 'warning',
60
- 'ERROR': 'danger',
61
- 'CRITICAL': 'danger'
62
- }
63
- color = colors.get(obj.level, 'secondary')
64
- return format_html(
65
- '<span class="badge badge-{}">{}</span>',
66
- color, obj.level
67
- )
68
-
69
- @display(description="Message")
70
- def message_preview(self, obj):
71
- """Display truncated message."""
72
- if len(obj.message) > 50:
73
- return obj.message[:50] + "..."
74
- return obj.message
75
-
76
-
77
- @admin.register(MaintenanceEvent)
78
- class MaintenanceEventAdmin(ModelAdmin, ImportExportModelAdmin):
79
- """Admin for MaintenanceEvent with Unfold styling."""
80
-
81
- list_display = [
82
- "title_with_icon",
83
- "status_badge",
84
- "sites_count",
85
- "duration_display",
86
- "initiated_by",
87
- "created_at_display"
88
- ]
89
- list_display_links = ["title_with_icon"]
90
- search_fields = ["title", "description", "initiated_by__email"]
91
- list_filter = [
92
- "status",
93
- "created_at",
94
- ("initiated_by", AutocompleteSelectFilter),
95
- ("sites", AutocompleteSelectMultipleFilter),
96
- ]
97
- ordering = ["-created_at"]
98
- readonly_fields = [
99
- "created_at", "updated_at"
100
- ]
101
-
102
- fieldsets = (
103
- ("Event Details", {
104
- "fields": ("title", "description", "reason", "initiated_by")
105
- }),
106
- ("Scheduling", {
107
- "fields": ("started_at", "ended_at", "estimated_duration", "status")
108
- }),
109
- ("Sites", {
110
- "fields": ("sites",)
111
- }),
112
- ("Timestamps", {
113
- "fields": ("created_at", "updated_at"),
114
- "classes": ("collapse",)
115
- })
116
- )
117
-
118
- filter_horizontal = ["sites"]
119
- inlines = [MaintenanceLogInline]
120
-
121
- # Unfold actions
122
- actions_detail = ["start_maintenance", "complete_maintenance", "cancel_maintenance"]
123
-
124
- @display(description="Event", ordering="title")
125
- def title_with_icon(self, obj):
126
- """Display event title with status icon."""
127
- icons = {
128
- 'scheduled': '📅',
129
- 'in_progress': '🔧',
130
- 'completed': '✅',
131
- 'failed': '❌',
132
- 'cancelled': '🚫'
133
- }
134
- icon = icons.get(obj.status, '📋')
135
- return format_html('{} {}', icon, obj.title)
136
-
137
- @display(description="Status", ordering="status")
138
- def status_badge(self, obj):
139
- """Display status with colored badge."""
140
- colors = {
141
- 'scheduled': 'info',
142
- 'in_progress': 'warning',
143
- 'completed': 'success',
144
- 'failed': 'danger',
145
- 'cancelled': 'secondary'
146
- }
147
- color = colors.get(obj.status, 'secondary')
148
- return format_html(
149
- '<span class="badge badge-{}">{}</span>',
150
- color, obj.get_status_display()
151
- )
152
-
153
- @display(description="Sites", ordering="sites_count")
154
- def sites_count(self, obj):
155
- """Display affected sites count."""
156
- count = obj.sites.count()
157
- if count > 0:
158
- return format_html(
159
- '<a href="{}?maintenance_events__id__exact={}" class="text-decoration-none">{} sites</a>',
160
- reverse('admin:django_cfg_maintenance_cloudflaresite_changelist'),
161
- obj.id, count
162
- )
163
- return "No sites"
164
-
165
- @display(description="Duration")
166
- def duration_display(self, obj):
167
- """Display event duration."""
168
- if obj.actual_start and obj.actual_end:
169
- duration = obj.actual_end - obj.actual_start
170
- hours = duration.total_seconds() / 3600
171
- if hours < 1:
172
- return f"{int(duration.total_seconds() / 60)}m"
173
- return f"{hours:.1f}h"
174
- elif obj.scheduled_start and obj.scheduled_end:
175
- duration = obj.scheduled_end - obj.scheduled_start
176
- hours = duration.total_seconds() / 3600
177
- if hours < 1:
178
- return f"{int(duration.total_seconds() / 60)}m (planned)"
179
- return f"{hours:.1f}h (planned)"
180
- return "-"
181
-
182
- @display(description="Scheduled Start", ordering="scheduled_start")
183
- def scheduled_start_display(self, obj):
184
- """Display scheduled start time."""
185
- if not obj.scheduled_start:
186
- return "-"
187
- return obj.scheduled_start.strftime("%Y-%m-%d %H:%M")
188
-
189
- @display(description="Created", ordering="created_at")
190
- def created_at_display(self, obj):
191
- """Display creation time."""
192
- return obj.created_at.strftime("%Y-%m-%d %H:%M")
193
-
194
- def get_queryset(self, request):
195
- """Optimize queryset with annotations."""
196
- return super().get_queryset(request).select_related(
197
- 'initiated_by', 'completed_by'
198
- ).prefetch_related(
199
- 'sites'
200
- ).annotate(
201
- sites_count=Count('sites')
202
- )
203
-
204
- @action(
205
- description="🚀 Start Maintenance",
206
- icon="play_arrow",
207
- variant=ActionVariant.WARNING
208
- )
209
- def start_maintenance(self, request, object_id):
210
- """Start maintenance event."""
211
- try:
212
- event = MaintenanceEvent.objects.get(id=object_id)
213
-
214
- if event.status != 'scheduled':
215
- messages.error(request, "Only scheduled events can be started")
216
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
217
-
218
- # Start maintenance logic here
219
- event.status = 'in_progress'
220
- event.save()
221
-
222
- messages.success(
223
- request,
224
- f"Maintenance event '{event.title}' has been started"
225
- )
226
-
227
- except Exception as e:
228
- messages.error(request, f"Failed to start maintenance: {str(e)}")
229
-
230
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
231
-
232
- @action(
233
- description="✅ Complete Maintenance",
234
- icon="check_circle",
235
- variant=ActionVariant.SUCCESS
236
- )
237
- def complete_maintenance(self, request, object_id):
238
- """Complete maintenance event."""
239
- try:
240
- event = MaintenanceEvent.objects.get(id=object_id)
241
-
242
- if event.status != 'in_progress':
243
- messages.error(request, "Only in-progress events can be completed")
244
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
245
-
246
- # Complete maintenance logic here
247
- event.status = 'completed'
248
- event.save()
249
-
250
- messages.success(
251
- request,
252
- f"Maintenance event '{event.title}' has been completed"
253
- )
254
-
255
- except Exception as e:
256
- messages.error(request, f"Failed to complete maintenance: {str(e)}")
257
-
258
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
259
-
260
- @action(
261
- description="🚫 Cancel Maintenance",
262
- icon="cancel",
263
- variant=ActionVariant.DANGER
264
- )
265
- def cancel_maintenance(self, request, object_id):
266
- """Cancel maintenance event."""
267
- try:
268
- event = MaintenanceEvent.objects.get(id=object_id)
269
-
270
- if event.status in ['completed', 'failed']:
271
- messages.error(request, "Cannot cancel completed or failed events")
272
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
273
-
274
- # Cancel maintenance logic here
275
- event.status = 'cancelled'
276
- event.save()
277
-
278
- messages.success(
279
- request,
280
- f"Maintenance event '{event.title}' has been cancelled"
281
- )
282
-
283
- except Exception as e:
284
- messages.error(request, f"Failed to cancel maintenance: {str(e)}")
285
-
286
- return redirect(request.META.get('HTTP_REFERER', '/admin/'))
287
-
288
- def save_model(self, request, obj, form, change):
289
- """Set initiated_by to current user if not set."""
290
- if not change:
291
- obj.initiated_by = request.user
292
- super().save_model(request, obj, form, change)
293
-
294
-
295
- @admin.register(MaintenanceLog)
296
- class MaintenanceLogAdmin(ModelAdmin):
297
- """Admin for MaintenanceLog with Unfold styling (read-only)."""
298
-
299
- list_display = [
300
- "timestamp_display",
301
- "level_badge",
302
- "component",
303
- "operation",
304
- "message_preview",
305
- "user",
306
- "maintenance_event_link"
307
- ]
308
- list_display_links = ["timestamp_display"]
309
- search_fields = ["message", "component", "operation", "user__email"]
310
- list_filter = [
311
- "level",
312
- "component",
313
- "operation",
314
- "timestamp",
315
- ("user", AutocompleteSelectFilter),
316
- ("maintenance_event", AutocompleteSelectFilter),
317
- ]
318
- ordering = ["-timestamp"]
319
- readonly_fields = [
320
- "timestamp", "level", "message", "component",
321
- "operation", "user"
322
- ]
323
-
324
- def has_add_permission(self, request):
325
- """Disable manual log creation."""
326
- return False
327
-
328
- def has_change_permission(self, request, obj=None):
329
- """Make logs read-only."""
330
- return False
331
-
332
- def has_delete_permission(self, request, obj=None):
333
- """Disable log deletion."""
334
- return False
335
-
336
- @display(description="Time", ordering="timestamp")
337
- def timestamp_display(self, obj):
338
- """Display timestamp."""
339
- return obj.timestamp.strftime("%Y-%m-%d %H:%M:%S")
340
-
341
- @display(description="Level", ordering="level")
342
- def level_badge(self, obj):
343
- """Display log level with colored badge."""
344
- colors = {
345
- 'DEBUG': 'secondary',
346
- 'INFO': 'info',
347
- 'WARNING': 'warning',
348
- 'ERROR': 'danger',
349
- 'CRITICAL': 'danger'
350
- }
351
- color = colors.get(obj.level, 'secondary')
352
- return format_html(
353
- '<span class="badge badge-{}">{}</span>',
354
- color, obj.level
355
- )
356
-
357
- @display(description="Message")
358
- def message_preview(self, obj):
359
- """Display truncated message."""
360
- if len(obj.message) > 100:
361
- return obj.message[:100] + "..."
362
- return obj.message
363
-
364
- @display(description="Event")
365
- def maintenance_event_link(self, obj):
366
- """Display link to maintenance event."""
367
- if obj.maintenance_event:
368
- return format_html(
369
- '<a href="{}" class="text-decoration-none">{}</a>',
370
- reverse('admin:django_cfg_maintenance_maintenanceevent_change',
371
- args=[obj.maintenance_event.id]),
372
- obj.maintenance_event.title
373
- )
374
- return "-"