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,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)