django-cfg 1.2.15__py3-none-any.whl → 1.2.17__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/maintenance/README.md +305 -0
- django_cfg/apps/maintenance/__init__.py +27 -0
- django_cfg/apps/maintenance/admin/__init__.py +28 -0
- django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
- django_cfg/apps/maintenance/admin/events_admin.py +374 -0
- django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
- django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
- django_cfg/apps/maintenance/apps.py +105 -0
- django_cfg/apps/maintenance/management/__init__.py +0 -0
- django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
- django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
- django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
- django_cfg/apps/maintenance/managers/__init__.py +20 -0
- django_cfg/apps/maintenance/managers/deployments.py +287 -0
- django_cfg/apps/maintenance/managers/events.py +374 -0
- django_cfg/apps/maintenance/managers/monitoring.py +301 -0
- django_cfg/apps/maintenance/managers/sites.py +335 -0
- django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
- django_cfg/apps/maintenance/migrations/__init__.py +0 -0
- django_cfg/apps/maintenance/models/__init__.py +27 -0
- django_cfg/apps/maintenance/models/cloudflare.py +316 -0
- django_cfg/apps/maintenance/models/maintenance.py +334 -0
- django_cfg/apps/maintenance/models/monitoring.py +393 -0
- django_cfg/apps/maintenance/models/sites.py +419 -0
- django_cfg/apps/maintenance/serializers/__init__.py +60 -0
- django_cfg/apps/maintenance/serializers/actions.py +310 -0
- django_cfg/apps/maintenance/serializers/base.py +44 -0
- django_cfg/apps/maintenance/serializers/deployments.py +209 -0
- django_cfg/apps/maintenance/serializers/events.py +210 -0
- django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
- django_cfg/apps/maintenance/serializers/sites.py +213 -0
- django_cfg/apps/maintenance/services/README.md +168 -0
- django_cfg/apps/maintenance/services/__init__.py +21 -0
- django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
- django_cfg/apps/maintenance/services/dns_manager.py +497 -0
- django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
- django_cfg/apps/maintenance/services/site_sync.py +448 -0
- django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
- django_cfg/apps/maintenance/services/worker_manager.py +264 -0
- django_cfg/apps/maintenance/signals.py +38 -0
- django_cfg/apps/maintenance/urls.py +36 -0
- django_cfg/apps/maintenance/views/__init__.py +18 -0
- django_cfg/apps/maintenance/views/base.py +61 -0
- django_cfg/apps/maintenance/views/deployments.py +175 -0
- django_cfg/apps/maintenance/views/events.py +204 -0
- django_cfg/apps/maintenance/views/monitoring.py +213 -0
- django_cfg/apps/maintenance/views/sites.py +338 -0
- django_cfg/apps/urls.py +5 -1
- django_cfg/core/config.py +34 -3
- django_cfg/core/generation.py +15 -10
- django_cfg/models/cloudflare.py +316 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +1 -1
- django_cfg/modules/base.py +12 -5
- django_cfg/modules/django_unfold/dashboard.py +16 -1
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/METADATA +2 -1
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/RECORD +61 -13
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,464 @@
|
|
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)
|
@@ -0,0 +1,105 @@
|
|
1
|
+
"""
|
2
|
+
Maintenance Application Configuration
|
3
|
+
|
4
|
+
Follows django-cfg patterns for app configuration with automatic setup.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from django.apps import AppConfig
|
8
|
+
from django.conf import settings
|
9
|
+
import logging
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class MaintenanceConfig(AppConfig):
|
15
|
+
"""Maintenance application configuration."""
|
16
|
+
|
17
|
+
default_auto_field = "django.db.models.BigAutoField"
|
18
|
+
name = "django_cfg.apps.maintenance"
|
19
|
+
label = "django_cfg_maintenance"
|
20
|
+
verbose_name = "Django CFG Maintenance"
|
21
|
+
|
22
|
+
def ready(self):
|
23
|
+
"""Initialize the maintenance application."""
|
24
|
+
# Import signal handlers
|
25
|
+
try:
|
26
|
+
import django_cfg.apps.maintenance.signals # noqa
|
27
|
+
except ImportError:
|
28
|
+
pass
|
29
|
+
|
30
|
+
# Auto-setup Cloudflare if configured
|
31
|
+
self._setup_cloudflare_auto_config()
|
32
|
+
|
33
|
+
def _setup_cloudflare_auto_config(self):
|
34
|
+
"""Auto-setup Cloudflare configuration if API token and domain are provided."""
|
35
|
+
try:
|
36
|
+
# Check if basic Cloudflare config is available
|
37
|
+
api_token = getattr(settings, 'CLOUDFLARE_API_TOKEN', None)
|
38
|
+
domain = getattr(settings, 'CLOUDFLARE_DOMAIN', None)
|
39
|
+
|
40
|
+
if api_token and domain:
|
41
|
+
logger.info("Cloudflare maintenance mode auto-configuration detected")
|
42
|
+
|
43
|
+
# Import here to avoid circular imports
|
44
|
+
from django_cfg.apps.maintenance.services.auto_setup import CloudflareAutoSetup
|
45
|
+
from django_cfg.models.cloudflare import CloudflareConfig
|
46
|
+
|
47
|
+
# Create configuration
|
48
|
+
config = CloudflareConfig(
|
49
|
+
api_token=api_token,
|
50
|
+
domain=domain
|
51
|
+
)
|
52
|
+
|
53
|
+
# Run auto-setup in background (non-blocking)
|
54
|
+
import asyncio
|
55
|
+
try:
|
56
|
+
# Try to get existing event loop
|
57
|
+
loop = asyncio.get_event_loop()
|
58
|
+
if loop.is_running():
|
59
|
+
# If loop is running, schedule the task
|
60
|
+
asyncio.create_task(self._run_auto_setup(config))
|
61
|
+
else:
|
62
|
+
# If no loop is running, run in new thread
|
63
|
+
import threading
|
64
|
+
thread = threading.Thread(
|
65
|
+
target=self._run_auto_setup_sync,
|
66
|
+
args=(config,)
|
67
|
+
)
|
68
|
+
thread.daemon = True
|
69
|
+
thread.start()
|
70
|
+
except RuntimeError:
|
71
|
+
# No event loop, run in thread
|
72
|
+
import threading
|
73
|
+
thread = threading.Thread(
|
74
|
+
target=self._run_auto_setup_sync,
|
75
|
+
args=(config,)
|
76
|
+
)
|
77
|
+
thread.daemon = True
|
78
|
+
thread.start()
|
79
|
+
|
80
|
+
except Exception as e:
|
81
|
+
logger.warning(f"Cloudflare auto-setup skipped: {e}")
|
82
|
+
|
83
|
+
async def _run_auto_setup(self, config):
|
84
|
+
"""Run auto-setup asynchronously."""
|
85
|
+
try:
|
86
|
+
from django_cfg.apps.maintenance.services.auto_setup import CloudflareAutoSetup
|
87
|
+
|
88
|
+
setup_service = CloudflareAutoSetup(config)
|
89
|
+
result = await setup_service.setup_complete_infrastructure()
|
90
|
+
|
91
|
+
if result.success:
|
92
|
+
logger.info(f"✅ Cloudflare auto-setup completed in {result.get_duration_seconds():.2f}s")
|
93
|
+
else:
|
94
|
+
logger.warning(f"❌ Cloudflare auto-setup failed: {len(result.get_failed_steps())} errors")
|
95
|
+
|
96
|
+
except Exception as e:
|
97
|
+
logger.error(f"Cloudflare auto-setup error: {e}")
|
98
|
+
|
99
|
+
def _run_auto_setup_sync(self, config):
|
100
|
+
"""Run auto-setup synchronously in thread."""
|
101
|
+
try:
|
102
|
+
import asyncio
|
103
|
+
asyncio.run(self._run_auto_setup(config))
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Cloudflare auto-setup thread error: {e}")
|
File without changes
|
File without changes
|