django-cfg 1.2.16__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/models/__init__.py +68 -0
- django_cfg/apps/accounts/models/activity.py +34 -0
- django_cfg/apps/accounts/models/auth.py +50 -0
- django_cfg/apps/accounts/models/base.py +8 -0
- django_cfg/apps/accounts/models/choices.py +32 -0
- django_cfg/apps/accounts/models/integrations.py +75 -0
- django_cfg/apps/accounts/models/registration.py +52 -0
- django_cfg/apps/accounts/models/user.py +80 -0
- django_cfg/apps/maintenance/__init__.py +53 -24
- django_cfg/apps/maintenance/admin/__init__.py +7 -18
- django_cfg/apps/maintenance/admin/api_key_admin.py +185 -0
- django_cfg/apps/maintenance/admin/log_admin.py +156 -0
- django_cfg/apps/maintenance/admin/scheduled_admin.py +390 -0
- django_cfg/apps/maintenance/admin/site_admin.py +448 -0
- django_cfg/apps/maintenance/apps.py +9 -96
- django_cfg/apps/maintenance/management/commands/maintenance.py +193 -307
- django_cfg/apps/maintenance/management/commands/process_scheduled_maintenance.py +241 -0
- django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +152 -111
- django_cfg/apps/maintenance/managers/__init__.py +7 -12
- django_cfg/apps/maintenance/managers/cloudflare_site_manager.py +192 -0
- django_cfg/apps/maintenance/managers/maintenance_log_manager.py +151 -0
- django_cfg/apps/maintenance/migrations/0001_initial.py +145 -705
- django_cfg/apps/maintenance/migrations/0002_cloudflaresite_maintenance_url.py +21 -0
- django_cfg/apps/maintenance/models/__init__.py +23 -21
- django_cfg/apps/maintenance/models/cloudflare_api_key.py +109 -0
- django_cfg/apps/maintenance/models/cloudflare_site.py +125 -0
- django_cfg/apps/maintenance/models/maintenance_log.py +131 -0
- django_cfg/apps/maintenance/models/scheduled_maintenance.py +307 -0
- django_cfg/apps/maintenance/services/__init__.py +37 -16
- django_cfg/apps/maintenance/services/bulk_operations_service.py +400 -0
- django_cfg/apps/maintenance/services/maintenance_service.py +230 -0
- django_cfg/apps/maintenance/services/scheduled_maintenance_service.py +381 -0
- django_cfg/apps/maintenance/services/site_sync_service.py +390 -0
- django_cfg/apps/maintenance/utils/__init__.py +12 -0
- django_cfg/apps/maintenance/utils/retry_utils.py +109 -0
- django_cfg/config.py +3 -0
- django_cfg/core/config.py +4 -6
- django_cfg/modules/django_unfold/dashboard.py +4 -5
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/METADATA +52 -1
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/RECORD +45 -55
- django_cfg/apps/maintenance/README.md +0 -305
- django_cfg/apps/maintenance/admin/deployments_admin.py +0 -251
- django_cfg/apps/maintenance/admin/events_admin.py +0 -374
- django_cfg/apps/maintenance/admin/monitoring_admin.py +0 -215
- django_cfg/apps/maintenance/admin/sites_admin.py +0 -464
- django_cfg/apps/maintenance/managers/deployments.py +0 -287
- django_cfg/apps/maintenance/managers/events.py +0 -374
- django_cfg/apps/maintenance/managers/monitoring.py +0 -301
- django_cfg/apps/maintenance/managers/sites.py +0 -335
- django_cfg/apps/maintenance/models/cloudflare.py +0 -316
- django_cfg/apps/maintenance/models/maintenance.py +0 -334
- django_cfg/apps/maintenance/models/monitoring.py +0 -393
- django_cfg/apps/maintenance/models/sites.py +0 -419
- django_cfg/apps/maintenance/serializers/__init__.py +0 -60
- django_cfg/apps/maintenance/serializers/actions.py +0 -310
- django_cfg/apps/maintenance/serializers/base.py +0 -44
- django_cfg/apps/maintenance/serializers/deployments.py +0 -209
- django_cfg/apps/maintenance/serializers/events.py +0 -210
- django_cfg/apps/maintenance/serializers/monitoring.py +0 -278
- django_cfg/apps/maintenance/serializers/sites.py +0 -213
- django_cfg/apps/maintenance/services/README.md +0 -168
- django_cfg/apps/maintenance/services/cloudflare_client.py +0 -441
- django_cfg/apps/maintenance/services/dns_manager.py +0 -497
- django_cfg/apps/maintenance/services/maintenance_manager.py +0 -504
- django_cfg/apps/maintenance/services/site_sync.py +0 -448
- django_cfg/apps/maintenance/services/sync_command_service.py +0 -330
- django_cfg/apps/maintenance/services/worker_manager.py +0 -264
- django_cfg/apps/maintenance/signals.py +0 -38
- django_cfg/apps/maintenance/urls.py +0 -36
- django_cfg/apps/maintenance/views/__init__.py +0 -18
- django_cfg/apps/maintenance/views/base.py +0 -61
- django_cfg/apps/maintenance/views/deployments.py +0 -175
- django_cfg/apps/maintenance/views/events.py +0 -204
- django_cfg/apps/maintenance/views/monitoring.py +0 -213
- django_cfg/apps/maintenance/views/sites.py +0 -338
- django_cfg/models/cloudflare.py +0 -316
- /django_cfg/apps/accounts/{models.py → __models.py} +0 -0
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.16.dist-info → django_cfg-1.2.18.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.16.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 "-"
|