django-cfg 1.2.17__py3-none-any.whl → 1.2.19__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 +4 -0
- django_cfg/core/config.py +4 -6
- django_cfg/modules/django_unfold/dashboard.py +4 -5
- {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/METADATA +52 -1
- {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.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.17.dist-info → django_cfg-1.2.19.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/licenses/LICENSE +0 -0
@@ -1,215 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Admin interface for MonitoringTarget model with Unfold features.
|
3
|
-
"""
|
4
|
-
|
5
|
-
from django.contrib import admin, messages
|
6
|
-
from django.utils.html import format_html
|
7
|
-
from django.urls import reverse
|
8
|
-
from django.shortcuts import redirect
|
9
|
-
from unfold.admin import ModelAdmin
|
10
|
-
from unfold.decorators import display, action
|
11
|
-
from unfold.enums import ActionVariant
|
12
|
-
from unfold.contrib.filters.admin import AutocompleteSelectFilter
|
13
|
-
from import_export.admin import ImportExportModelAdmin
|
14
|
-
|
15
|
-
from ..models import MonitoringTarget
|
16
|
-
from ..services import MaintenanceManager
|
17
|
-
from django.contrib.auth import get_user_model
|
18
|
-
|
19
|
-
User = get_user_model()
|
20
|
-
|
21
|
-
|
22
|
-
@admin.register(MonitoringTarget)
|
23
|
-
class MonitoringTargetAdmin(ModelAdmin, ImportExportModelAdmin):
|
24
|
-
"""Admin for MonitoringTarget with Unfold styling."""
|
25
|
-
|
26
|
-
list_display = [
|
27
|
-
"site_with_icon",
|
28
|
-
"url_display",
|
29
|
-
"status_badge",
|
30
|
-
"last_check_display",
|
31
|
-
"check_interval_display",
|
32
|
-
"failure_count_display"
|
33
|
-
]
|
34
|
-
list_display_links = ["site_with_icon", "url_display"]
|
35
|
-
search_fields = ["site__name", "site__domain", "check_url"]
|
36
|
-
list_filter = [
|
37
|
-
"status",
|
38
|
-
"check_interval",
|
39
|
-
"failure_threshold",
|
40
|
-
"last_check_at",
|
41
|
-
("site", AutocompleteSelectFilter),
|
42
|
-
]
|
43
|
-
ordering = ["-last_check_at"]
|
44
|
-
readonly_fields = [
|
45
|
-
"last_check_at", "status", "last_check_success",
|
46
|
-
"created_at", "updated_at"
|
47
|
-
]
|
48
|
-
|
49
|
-
fieldsets = (
|
50
|
-
("Target Configuration", {
|
51
|
-
"fields": ("site", "check_url", "status")
|
52
|
-
}),
|
53
|
-
("Monitoring Settings", {
|
54
|
-
"fields": ("check_interval", "timeout", "expected_status_codes", "failure_threshold", "recovery_threshold")
|
55
|
-
}),
|
56
|
-
("Response Validation", {
|
57
|
-
"fields": ("expected_content", "expected_response_time_ms"),
|
58
|
-
"classes": ("collapse",)
|
59
|
-
}),
|
60
|
-
("Status Information", {
|
61
|
-
"fields": ("consecutive_failures", "consecutive_successes", "last_check_at", "last_check_success"),
|
62
|
-
"classes": ("collapse",)
|
63
|
-
}),
|
64
|
-
("Timestamps", {
|
65
|
-
"fields": ("created_at", "updated_at"),
|
66
|
-
"classes": ("collapse",)
|
67
|
-
})
|
68
|
-
)
|
69
|
-
|
70
|
-
# Unfold actions
|
71
|
-
actions_detail = ["run_health_check", "enable_monitoring", "disable_monitoring"]
|
72
|
-
|
73
|
-
@display(description="Site", ordering="site__name")
|
74
|
-
def site_with_icon(self, obj):
|
75
|
-
"""Display site name with icon."""
|
76
|
-
return format_html('🌐 {}', obj.site.name)
|
77
|
-
|
78
|
-
@display(description="URL", ordering="check_url")
|
79
|
-
def url_display(self, obj):
|
80
|
-
"""Display monitoring URL."""
|
81
|
-
url = obj.check_url
|
82
|
-
if len(url) > 50:
|
83
|
-
url = url[:47] + "..."
|
84
|
-
return format_html('<code>{}</code>', url)
|
85
|
-
|
86
|
-
@display(
|
87
|
-
description="Status",
|
88
|
-
ordering="status",
|
89
|
-
label={
|
90
|
-
'active': 'success',
|
91
|
-
'paused': 'warning',
|
92
|
-
'disabled': 'secondary',
|
93
|
-
'error': 'danger'
|
94
|
-
}
|
95
|
-
)
|
96
|
-
def status_badge(self, obj):
|
97
|
-
"""Display monitoring status."""
|
98
|
-
return obj.status, obj.get_status_display()
|
99
|
-
|
100
|
-
@display(description="Last Check", ordering="last_check_at")
|
101
|
-
def last_check_display(self, obj):
|
102
|
-
"""Display last check time."""
|
103
|
-
if obj.last_check_at:
|
104
|
-
return obj.last_check_at.strftime("%Y-%m-%d %H:%M")
|
105
|
-
return "Never"
|
106
|
-
|
107
|
-
@display(description="Check Interval", ordering="check_interval")
|
108
|
-
def check_interval_display(self, obj):
|
109
|
-
"""Display check interval in human readable format."""
|
110
|
-
seconds = obj.check_interval
|
111
|
-
if seconds >= 3600:
|
112
|
-
return f"{seconds // 3600}h {(seconds % 3600) // 60}m"
|
113
|
-
elif seconds >= 60:
|
114
|
-
return f"{seconds // 60}m"
|
115
|
-
return f"{seconds}s"
|
116
|
-
|
117
|
-
@display(description="Failures", ordering="consecutive_failures")
|
118
|
-
def failure_count_display(self, obj):
|
119
|
-
"""Display failure count with threshold."""
|
120
|
-
return f"{obj.consecutive_failures}/{obj.failure_threshold}"
|
121
|
-
|
122
|
-
def get_queryset(self, request):
|
123
|
-
"""Filter queryset based on user permissions."""
|
124
|
-
qs = super().get_queryset(request)
|
125
|
-
if request.user.is_superuser:
|
126
|
-
return qs
|
127
|
-
return qs.filter(site__owner=request.user)
|
128
|
-
|
129
|
-
def save_model(self, request, obj, form, change):
|
130
|
-
"""Set owner to current user if not set."""
|
131
|
-
if not change and not obj.site.owner:
|
132
|
-
obj.site.owner = request.user
|
133
|
-
super().save_model(request, obj, form, change)
|
134
|
-
|
135
|
-
@action(
|
136
|
-
description="🩺 Run Health Check",
|
137
|
-
icon="health_and_safety",
|
138
|
-
variant=ActionVariant.INFO
|
139
|
-
)
|
140
|
-
def run_health_check(self, request, object_id):
|
141
|
-
"""Run immediate health check."""
|
142
|
-
try:
|
143
|
-
target = self.get_object(request, object_id)
|
144
|
-
if not target:
|
145
|
-
messages.error(request, "Monitoring target not found.")
|
146
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
147
|
-
|
148
|
-
# TODO: Implement actual health check logic
|
149
|
-
messages.success(
|
150
|
-
request,
|
151
|
-
f"Health check queued for {target.check_url}."
|
152
|
-
)
|
153
|
-
|
154
|
-
except Exception as e:
|
155
|
-
import logging
|
156
|
-
logger = logging.getLogger(__name__)
|
157
|
-
logger.exception(f"Health check failed for target {object_id}")
|
158
|
-
messages.error(
|
159
|
-
request,
|
160
|
-
f"Health check failed: {e}"
|
161
|
-
)
|
162
|
-
|
163
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
164
|
-
|
165
|
-
@action(
|
166
|
-
description="▶️ Enable Monitoring",
|
167
|
-
icon="play_arrow",
|
168
|
-
variant=ActionVariant.SUCCESS
|
169
|
-
)
|
170
|
-
def enable_monitoring(self, request, object_id):
|
171
|
-
"""Enable monitoring for target."""
|
172
|
-
try:
|
173
|
-
target = self.get_object(request, object_id)
|
174
|
-
if not target:
|
175
|
-
messages.error(request, "Monitoring target not found.")
|
176
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
177
|
-
|
178
|
-
target.status = MonitoringTarget.Status.ACTIVE
|
179
|
-
target.save(update_fields=['status'])
|
180
|
-
|
181
|
-
messages.success(
|
182
|
-
request,
|
183
|
-
f"Monitoring enabled for {target.check_url}."
|
184
|
-
)
|
185
|
-
|
186
|
-
except Exception as e:
|
187
|
-
messages.error(request, f"Failed to enable monitoring: {e}")
|
188
|
-
|
189
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
190
|
-
|
191
|
-
@action(
|
192
|
-
description="⏸️ Disable Monitoring",
|
193
|
-
icon="pause",
|
194
|
-
variant=ActionVariant.WARNING
|
195
|
-
)
|
196
|
-
def disable_monitoring(self, request, object_id):
|
197
|
-
"""Disable monitoring for target."""
|
198
|
-
try:
|
199
|
-
target = self.get_object(request, object_id)
|
200
|
-
if not target:
|
201
|
-
messages.error(request, "Monitoring target not found.")
|
202
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
203
|
-
|
204
|
-
target.status = MonitoringTarget.Status.DISABLED
|
205
|
-
target.save(update_fields=['status'])
|
206
|
-
|
207
|
-
messages.success(
|
208
|
-
request,
|
209
|
-
f"Monitoring disabled for {target.check_url}."
|
210
|
-
)
|
211
|
-
|
212
|
-
except Exception as e:
|
213
|
-
messages.error(request, f"Failed to disable monitoring: {e}")
|
214
|
-
|
215
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
@@ -1,464 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Sites 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 CloudflareSite, SiteGroup, MaintenanceEvent, MonitoringTarget
|
20
|
-
from ..services import SiteSyncService, MaintenanceManager
|
21
|
-
|
22
|
-
|
23
|
-
class MaintenanceEventInline(TabularInline):
|
24
|
-
"""Inline for maintenance events with Unfold styling."""
|
25
|
-
|
26
|
-
model = MaintenanceEvent.sites.through
|
27
|
-
verbose_name = "Maintenance Event"
|
28
|
-
verbose_name_plural = "🔧 Recent Maintenance Events"
|
29
|
-
extra = 0
|
30
|
-
max_num = 5
|
31
|
-
can_delete = False
|
32
|
-
show_change_link = False
|
33
|
-
fields = []
|
34
|
-
readonly_fields = []
|
35
|
-
|
36
|
-
|
37
|
-
class MonitoringTargetInline(TabularInline):
|
38
|
-
"""Inline for monitoring targets with Unfold styling."""
|
39
|
-
|
40
|
-
model = MonitoringTarget
|
41
|
-
verbose_name = "Monitoring Target"
|
42
|
-
verbose_name_plural = "📊 Monitoring Configuration"
|
43
|
-
extra = 0
|
44
|
-
max_num = 1
|
45
|
-
|
46
|
-
fields = ['check_url', 'check_interval', 'status_display', 'last_check_display']
|
47
|
-
readonly_fields = ['status_display', 'last_check_display']
|
48
|
-
|
49
|
-
@display(description="Status")
|
50
|
-
def status_display(self, obj):
|
51
|
-
"""Display monitoring status with badge."""
|
52
|
-
if not obj.status:
|
53
|
-
return format_html('<span class="badge badge-secondary">Unknown</span>')
|
54
|
-
|
55
|
-
colors = {
|
56
|
-
'active': 'success',
|
57
|
-
'paused': 'warning',
|
58
|
-
'disabled': 'secondary',
|
59
|
-
'error': 'danger'
|
60
|
-
}
|
61
|
-
color = colors.get(obj.status, 'secondary')
|
62
|
-
return format_html(
|
63
|
-
'<span class="badge badge-{}">{}</span>',
|
64
|
-
color, obj.get_status_display()
|
65
|
-
)
|
66
|
-
|
67
|
-
@display(description="Last Check")
|
68
|
-
def last_check_display(self, obj):
|
69
|
-
"""Display last check time."""
|
70
|
-
if not obj.last_check_at:
|
71
|
-
return "Never"
|
72
|
-
return obj.last_check_at.strftime("%Y-%m-%d %H:%M")
|
73
|
-
|
74
|
-
|
75
|
-
@admin.register(CloudflareSite)
|
76
|
-
class CloudflareSiteAdmin(ModelAdmin, ImportExportModelAdmin):
|
77
|
-
"""Admin for CloudflareSite with Unfold styling."""
|
78
|
-
|
79
|
-
# Unfold configuration
|
80
|
-
list_display = [
|
81
|
-
"name_with_icon",
|
82
|
-
"domain",
|
83
|
-
"environment_badge",
|
84
|
-
"status_badge",
|
85
|
-
"zone_info",
|
86
|
-
"monitoring_status",
|
87
|
-
"events_count",
|
88
|
-
"owner",
|
89
|
-
"last_maintenance_display"
|
90
|
-
]
|
91
|
-
list_display_links = ["name_with_icon", "domain"]
|
92
|
-
search_fields = ["name", "domain", "zone_id", "owner__email"]
|
93
|
-
list_filter = [
|
94
|
-
"environment",
|
95
|
-
"current_status",
|
96
|
-
"created_at",
|
97
|
-
("owner", AutocompleteSelectFilter),
|
98
|
-
]
|
99
|
-
ordering = ["-created_at"]
|
100
|
-
readonly_fields = [
|
101
|
-
"zone_id", "account_id", "current_status", "maintenance_active",
|
102
|
-
"last_status_check", "last_maintenance_at", "created_at", "updated_at"
|
103
|
-
]
|
104
|
-
|
105
|
-
fieldsets = (
|
106
|
-
("Basic Information", {
|
107
|
-
"fields": ("name", "domain", "description", "owner")
|
108
|
-
}),
|
109
|
-
("Configuration", {
|
110
|
-
"fields": ("environment", "project", "tags")
|
111
|
-
}),
|
112
|
-
("Cloudflare Integration", {
|
113
|
-
"fields": ("zone_id", "account_id", "api_token"),
|
114
|
-
"classes": ("collapse",)
|
115
|
-
}),
|
116
|
-
("Maintenance Settings", {
|
117
|
-
"fields": ("worker_name", "maintenance_template", "custom_maintenance_message"),
|
118
|
-
"classes": ("collapse",)
|
119
|
-
}),
|
120
|
-
("Monitoring", {
|
121
|
-
"fields": ("monitoring_enabled", "health_check_url", "check_interval"),
|
122
|
-
"classes": ("collapse",)
|
123
|
-
}),
|
124
|
-
("Status", {
|
125
|
-
"fields": ("current_status", "maintenance_active", "last_status_check", "last_maintenance_at"),
|
126
|
-
"classes": ("collapse",)
|
127
|
-
}),
|
128
|
-
("Access Control", {
|
129
|
-
"fields": ("allowed_users",),
|
130
|
-
"classes": ("collapse",)
|
131
|
-
}),
|
132
|
-
("Timestamps", {
|
133
|
-
"fields": ("created_at", "updated_at"),
|
134
|
-
"classes": ("collapse",)
|
135
|
-
})
|
136
|
-
)
|
137
|
-
|
138
|
-
inlines = [MonitoringTargetInline, MaintenanceEventInline]
|
139
|
-
|
140
|
-
# Unfold actions
|
141
|
-
actions_detail = ["sync_with_cloudflare", "enable_maintenance", "disable_maintenance"]
|
142
|
-
actions_list = ["bulk_sync_sites"]
|
143
|
-
|
144
|
-
@display(description="Site", ordering="name")
|
145
|
-
def name_with_icon(self, obj):
|
146
|
-
"""Display site name with icon."""
|
147
|
-
icon = "🌐"
|
148
|
-
if obj.environment == "production":
|
149
|
-
icon = "🚀"
|
150
|
-
elif obj.environment == "staging":
|
151
|
-
icon = "🧪"
|
152
|
-
elif obj.environment == "development":
|
153
|
-
icon = "🔧"
|
154
|
-
|
155
|
-
return format_html('{} {}', icon, obj.name)
|
156
|
-
|
157
|
-
@display(description="Environment", ordering="environment")
|
158
|
-
def environment_badge(self, obj):
|
159
|
-
"""Display environment with colored badge."""
|
160
|
-
colors = {
|
161
|
-
'production': 'success',
|
162
|
-
'staging': 'warning',
|
163
|
-
'development': 'info',
|
164
|
-
'testing': 'secondary'
|
165
|
-
}
|
166
|
-
color = colors.get(obj.environment, 'secondary')
|
167
|
-
return format_html(
|
168
|
-
'<span class="badge badge-{}">{}</span>',
|
169
|
-
color, obj.get_environment_display()
|
170
|
-
)
|
171
|
-
|
172
|
-
@display(description="Status", ordering="current_status")
|
173
|
-
def status_badge(self, obj):
|
174
|
-
"""Display status with colored badge."""
|
175
|
-
colors = {
|
176
|
-
'active': 'success',
|
177
|
-
'maintenance': 'warning',
|
178
|
-
'offline': 'danger',
|
179
|
-
'unknown': 'secondary'
|
180
|
-
}
|
181
|
-
color = colors.get(obj.current_status, 'secondary')
|
182
|
-
return format_html(
|
183
|
-
'<span class="badge badge-{}">{}</span>',
|
184
|
-
color, obj.get_current_status_display()
|
185
|
-
)
|
186
|
-
|
187
|
-
@display(description="Zone Info")
|
188
|
-
def zone_info(self, obj):
|
189
|
-
"""Display Cloudflare zone information."""
|
190
|
-
if not obj.zone_id:
|
191
|
-
return format_html('<span class="text-muted">Not synced</span>')
|
192
|
-
|
193
|
-
return format_html(
|
194
|
-
'<small class="text-muted">Zone: {}</small>',
|
195
|
-
obj.zone_id[:8] + "..." if len(obj.zone_id) > 8 else obj.zone_id
|
196
|
-
)
|
197
|
-
|
198
|
-
@display(description="Monitoring")
|
199
|
-
def monitoring_status(self, obj):
|
200
|
-
"""Display monitoring status."""
|
201
|
-
try:
|
202
|
-
target = obj.monitoring_target
|
203
|
-
if target.enabled:
|
204
|
-
color = 'success' if target.status == 'healthy' else 'danger'
|
205
|
-
return format_html(
|
206
|
-
'<span class="badge badge-{}">{}</span>',
|
207
|
-
color, target.status or 'Unknown'
|
208
|
-
)
|
209
|
-
else:
|
210
|
-
return format_html('<span class="text-muted">Disabled</span>')
|
211
|
-
except:
|
212
|
-
return format_html('<span class="text-muted">Not configured</span>')
|
213
|
-
|
214
|
-
@display(description="Events", ordering="maintenance_events_count")
|
215
|
-
def events_count(self, obj):
|
216
|
-
"""Display maintenance events count."""
|
217
|
-
count = obj.maintenance_events.count()
|
218
|
-
if count > 0:
|
219
|
-
return format_html(
|
220
|
-
'<a href="{}?sites__id__exact={}" class="text-decoration-none">{} events</a>',
|
221
|
-
reverse('admin:django_cfg_maintenance_maintenanceevent_changelist'),
|
222
|
-
obj.id, count
|
223
|
-
)
|
224
|
-
return "No events"
|
225
|
-
|
226
|
-
@display(description="Last Maintenance", ordering="last_maintenance_at")
|
227
|
-
def last_maintenance_display(self, obj):
|
228
|
-
"""Display last maintenance time."""
|
229
|
-
if not obj.last_maintenance_at:
|
230
|
-
return format_html('<span class="text-muted">Never</span>')
|
231
|
-
return obj.last_maintenance_at.strftime("%Y-%m-%d %H:%M")
|
232
|
-
|
233
|
-
def get_queryset(self, request):
|
234
|
-
"""Optimize queryset with annotations."""
|
235
|
-
return super().get_queryset(request).select_related(
|
236
|
-
'owner'
|
237
|
-
).prefetch_related(
|
238
|
-
'maintenance_events'
|
239
|
-
).annotate(
|
240
|
-
maintenance_events_count=Count('maintenance_events')
|
241
|
-
)
|
242
|
-
|
243
|
-
@action(
|
244
|
-
description="🔄 Sync with Cloudflare",
|
245
|
-
icon="refresh",
|
246
|
-
variant=ActionVariant.INFO
|
247
|
-
)
|
248
|
-
def sync_with_cloudflare(self, request, object_id):
|
249
|
-
"""Sync site with Cloudflare zones."""
|
250
|
-
try:
|
251
|
-
# Get the site object to get its name for the message
|
252
|
-
site = self.get_object(request, object_id)
|
253
|
-
if not site:
|
254
|
-
messages.error(request, "Site not found.")
|
255
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
256
|
-
|
257
|
-
# TODO: Implement actual sync logic here
|
258
|
-
# For now, just update the last_status_check timestamp
|
259
|
-
from django.utils import timezone
|
260
|
-
site.last_status_check = timezone.now()
|
261
|
-
site.save(update_fields=['last_status_check'])
|
262
|
-
|
263
|
-
messages.success(
|
264
|
-
request,
|
265
|
-
f"Site '{site.name}' has been queued for synchronization with Cloudflare.",
|
266
|
-
)
|
267
|
-
|
268
|
-
except CloudflareSite.DoesNotExist:
|
269
|
-
messages.error(
|
270
|
-
request,
|
271
|
-
"Site not found.",
|
272
|
-
)
|
273
|
-
except Exception as e:
|
274
|
-
import logging
|
275
|
-
logger = logging.getLogger(__name__)
|
276
|
-
logger.exception(f"Unexpected error syncing site {object_id}")
|
277
|
-
messages.error(
|
278
|
-
request,
|
279
|
-
f"Unexpected error syncing site: {e}",
|
280
|
-
)
|
281
|
-
|
282
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
283
|
-
|
284
|
-
@action(
|
285
|
-
description="🔧 Enable Maintenance",
|
286
|
-
icon="build",
|
287
|
-
variant=ActionVariant.WARNING
|
288
|
-
)
|
289
|
-
def enable_maintenance(self, request, object_id):
|
290
|
-
"""Enable maintenance mode for a site."""
|
291
|
-
try:
|
292
|
-
site = self.get_object(request, object_id)
|
293
|
-
if not site:
|
294
|
-
messages.error(request, "Site not found.")
|
295
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
296
|
-
|
297
|
-
manager = MaintenanceManager(request.user)
|
298
|
-
manager.enable_maintenance_mode(site)
|
299
|
-
messages.success(request, f"Maintenance mode enabled for {site.name}.")
|
300
|
-
except Exception as e:
|
301
|
-
messages.error(request, f"Failed to enable maintenance: {str(e)}")
|
302
|
-
|
303
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
304
|
-
|
305
|
-
@action(
|
306
|
-
description="✅ Disable Maintenance",
|
307
|
-
icon="check_circle",
|
308
|
-
variant=ActionVariant.SUCCESS
|
309
|
-
)
|
310
|
-
def disable_maintenance(self, request, object_id):
|
311
|
-
"""Disable maintenance mode for a site."""
|
312
|
-
try:
|
313
|
-
site = self.get_object(request, object_id)
|
314
|
-
if not site:
|
315
|
-
messages.error(request, "Site not found.")
|
316
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
317
|
-
|
318
|
-
manager = MaintenanceManager(request.user)
|
319
|
-
manager.disable_maintenance_mode(site)
|
320
|
-
messages.success(request, f"Maintenance mode disabled for {site.name}.")
|
321
|
-
except Exception as e:
|
322
|
-
messages.error(request, f"Failed to disable maintenance: {str(e)}")
|
323
|
-
|
324
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
325
|
-
|
326
|
-
def save_model(self, request, obj, form, change):
|
327
|
-
"""Set owner to current user if not set."""
|
328
|
-
if not change:
|
329
|
-
obj.owner = request.user
|
330
|
-
super().save_model(request, obj, form, change)
|
331
|
-
|
332
|
-
@action(
|
333
|
-
description="🔄 Sync All Sites with Cloudflare",
|
334
|
-
icon="sync",
|
335
|
-
variant=ActionVariant.INFO,
|
336
|
-
url_path="bulk-sync-sites",
|
337
|
-
permissions=["bulk_sync_sites"]
|
338
|
-
)
|
339
|
-
def bulk_sync_sites(self, request):
|
340
|
-
"""Bulk sync all sites with Cloudflare."""
|
341
|
-
try:
|
342
|
-
from django.utils import timezone
|
343
|
-
from django_cfg.apps.maintenance.services import SiteSyncService
|
344
|
-
|
345
|
-
# Get all sites for the current user (or all if superuser)
|
346
|
-
if request.user.is_superuser:
|
347
|
-
sites = CloudflareSite.objects.all()
|
348
|
-
else:
|
349
|
-
sites = CloudflareSite.objects.filter(owner=request.user)
|
350
|
-
|
351
|
-
count = sites.count()
|
352
|
-
if count == 0:
|
353
|
-
messages.warning(request, "No sites found to synchronize.")
|
354
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
355
|
-
|
356
|
-
# Update last_status_check for all sites
|
357
|
-
sites.update(last_status_check=timezone.now())
|
358
|
-
|
359
|
-
# TODO: Implement actual bulk sync logic here using SiteSyncService
|
360
|
-
# This would typically queue background tasks for each site
|
361
|
-
|
362
|
-
messages.success(
|
363
|
-
request,
|
364
|
-
f"Successfully queued {count} sites for Cloudflare synchronization."
|
365
|
-
)
|
366
|
-
|
367
|
-
except Exception as e:
|
368
|
-
import logging
|
369
|
-
logger = logging.getLogger(__name__)
|
370
|
-
logger.exception("Bulk sync failed")
|
371
|
-
messages.error(
|
372
|
-
request,
|
373
|
-
f"Bulk sync failed: {e}"
|
374
|
-
)
|
375
|
-
|
376
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
377
|
-
|
378
|
-
def has_bulk_sync_sites_permission(self, request):
|
379
|
-
"""Check if user has permission to bulk sync sites."""
|
380
|
-
return request.user.is_staff
|
381
|
-
|
382
|
-
|
383
|
-
@admin.register(SiteGroup)
|
384
|
-
class SiteGroupAdmin(ModelAdmin, ImportExportModelAdmin):
|
385
|
-
"""Admin for SiteGroup with Unfold styling."""
|
386
|
-
|
387
|
-
list_display = [
|
388
|
-
"name_with_icon",
|
389
|
-
"description_short",
|
390
|
-
"sites_count",
|
391
|
-
"owner",
|
392
|
-
"created_at_display"
|
393
|
-
]
|
394
|
-
list_display_links = ["name_with_icon"]
|
395
|
-
search_fields = ["name", "description", "owner__email"]
|
396
|
-
list_filter = [
|
397
|
-
"created_at",
|
398
|
-
("owner", AutocompleteSelectFilter),
|
399
|
-
]
|
400
|
-
ordering = ["-created_at"]
|
401
|
-
readonly_fields = ["created_at", "updated_at"]
|
402
|
-
|
403
|
-
fieldsets = (
|
404
|
-
("Basic Information", {
|
405
|
-
"fields": ("name", "description", "owner")
|
406
|
-
}),
|
407
|
-
("Sites", {
|
408
|
-
"fields": ("sites",)
|
409
|
-
}),
|
410
|
-
("Timestamps", {
|
411
|
-
"fields": ("created_at", "updated_at"),
|
412
|
-
"classes": ("collapse",)
|
413
|
-
})
|
414
|
-
)
|
415
|
-
|
416
|
-
filter_horizontal = ["sites"]
|
417
|
-
|
418
|
-
@display(description="Group", ordering="name")
|
419
|
-
def name_with_icon(self, obj):
|
420
|
-
"""Display group name with icon."""
|
421
|
-
return format_html('📁 {}', obj.name)
|
422
|
-
|
423
|
-
@display(description="Description")
|
424
|
-
def description_short(self, obj):
|
425
|
-
"""Display truncated description."""
|
426
|
-
if not obj.description:
|
427
|
-
return format_html('<span class="text-muted">No description</span>')
|
428
|
-
|
429
|
-
if len(obj.description) > 50:
|
430
|
-
return obj.description[:50] + "..."
|
431
|
-
return obj.description
|
432
|
-
|
433
|
-
@display(description="Sites", ordering="sites_count")
|
434
|
-
def sites_count(self, obj):
|
435
|
-
"""Display sites count with link."""
|
436
|
-
count = obj.sites.count()
|
437
|
-
if count > 0:
|
438
|
-
return format_html(
|
439
|
-
'<a href="{}?groups__id__exact={}" class="text-decoration-none">{} sites</a>',
|
440
|
-
reverse('admin:django_cfg_maintenance_cloudflaresite_changelist'),
|
441
|
-
obj.id, count
|
442
|
-
)
|
443
|
-
return "No sites"
|
444
|
-
|
445
|
-
@display(description="Created", ordering="created_at")
|
446
|
-
def created_at_display(self, obj):
|
447
|
-
"""Display creation time."""
|
448
|
-
return obj.created_at.strftime("%Y-%m-%d %H:%M")
|
449
|
-
|
450
|
-
def get_queryset(self, request):
|
451
|
-
"""Optimize queryset with annotations."""
|
452
|
-
return super().get_queryset(request).select_related(
|
453
|
-
'owner'
|
454
|
-
).prefetch_related(
|
455
|
-
'sites'
|
456
|
-
).annotate(
|
457
|
-
sites_count=Count('sites')
|
458
|
-
)
|
459
|
-
|
460
|
-
def save_model(self, request, obj, form, change):
|
461
|
-
"""Set owner to current user if not set."""
|
462
|
-
if not change:
|
463
|
-
obj.owner = request.user
|
464
|
-
super().save_model(request, obj, form, change)
|