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.
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 +4 -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.19.dist-info}/METADATA +52 -1
  41. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.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.19.dist-info}/WHEEL +0 -0
  80. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/entry_points.txt +0 -0
  81. {django_cfg-1.2.17.dist-info → django_cfg-1.2.19.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,390 @@
1
+ """
2
+ Admin interface for ScheduledMaintenance with Unfold styling.
3
+
4
+ Provides comprehensive management of scheduled maintenance events.
5
+ """
6
+
7
+ from django.contrib import admin
8
+ from django.utils.html import format_html
9
+ from django.http import HttpRequest, HttpResponse
10
+ from django.shortcuts import redirect
11
+ from django.contrib import messages
12
+ from django.utils import timezone
13
+ from django.urls import path, reverse
14
+ from django.template.response import TemplateResponse
15
+ from typing import Any
16
+
17
+ from unfold.admin import ModelAdmin
18
+ from unfold.decorators import display, action
19
+ from unfold.enums import ActionVariant
20
+
21
+ from ..models import ScheduledMaintenance, CloudflareSite
22
+
23
+
24
+ @admin.register(ScheduledMaintenance)
25
+ class ScheduledMaintenanceAdmin(ModelAdmin):
26
+ """Admin for ScheduledMaintenance with Unfold styling."""
27
+
28
+ list_display = [
29
+ "status_display",
30
+ "title",
31
+ "scheduled_start",
32
+ "duration_display",
33
+ "sites_count",
34
+ "priority_badge",
35
+ "auto_flags",
36
+ "created_at",
37
+ ]
38
+ list_display_links = ["title"]
39
+ search_fields = ["title", "description", "maintenance_message"]
40
+ list_filter = [
41
+ "status",
42
+ "priority",
43
+ "auto_enable",
44
+ "auto_disable",
45
+ "scheduled_start",
46
+ "created_at"
47
+ ]
48
+ ordering = ["-scheduled_start"]
49
+
50
+ fieldsets = [
51
+ ("Basic Information", {
52
+ 'fields': ['title', 'description', 'priority', 'created_by']
53
+ }),
54
+ ("Scheduling", {
55
+ 'fields': ['scheduled_start', 'estimated_duration', 'scheduled_end']
56
+ }),
57
+ ("Sites", {
58
+ 'fields': ['sites']
59
+ }),
60
+ ("Configuration", {
61
+ 'fields': ['maintenance_message', 'template', 'auto_enable', 'auto_disable']
62
+ }),
63
+ ("Notifications", {
64
+ 'fields': ['notify_before', 'notify_on_start', 'notify_on_complete'],
65
+ 'classes': ['collapse']
66
+ }),
67
+ ("Execution Status", {
68
+ 'fields': ['status', 'actual_start', 'actual_end'],
69
+ 'classes': ['collapse']
70
+ }),
71
+ ("Execution Log", {
72
+ 'fields': ['execution_log'],
73
+ 'classes': ['collapse']
74
+ }),
75
+ ]
76
+
77
+ readonly_fields = ['scheduled_end', 'actual_start', 'actual_end', 'execution_log']
78
+
79
+ filter_horizontal = ['sites']
80
+
81
+ actions = [
82
+ 'start_maintenance_action',
83
+ 'complete_maintenance_action',
84
+ 'cancel_maintenance_action',
85
+ 'duplicate_maintenance_action'
86
+ ]
87
+
88
+ def get_urls(self):
89
+ """Add custom admin URLs."""
90
+ urls = super().get_urls()
91
+ custom_urls = [
92
+ path(
93
+ 'calendar/',
94
+ self.admin_site.admin_view(self.calendar_view),
95
+ name='scheduled_maintenance_calendar'
96
+ ),
97
+ path(
98
+ '<int:object_id>/start/',
99
+ self.admin_site.admin_view(self.start_maintenance_view),
100
+ name='scheduled_maintenance_start'
101
+ ),
102
+ path(
103
+ '<int:object_id>/complete/',
104
+ self.admin_site.admin_view(self.complete_maintenance_view),
105
+ name='scheduled_maintenance_complete'
106
+ ),
107
+ ]
108
+ return custom_urls + urls
109
+
110
+ @display(description="Status")
111
+ def status_display(self, obj: ScheduledMaintenance) -> str:
112
+ """Display status with colored badge and timing info."""
113
+ status_config = {
114
+ ScheduledMaintenance.Status.SCHEDULED: {
115
+ 'emoji': '📅',
116
+ 'color': 'blue',
117
+ 'text': 'Scheduled'
118
+ },
119
+ ScheduledMaintenance.Status.ACTIVE: {
120
+ 'emoji': '🔧',
121
+ 'color': 'orange',
122
+ 'text': 'Active'
123
+ },
124
+ ScheduledMaintenance.Status.COMPLETED: {
125
+ 'emoji': '✅',
126
+ 'color': 'green',
127
+ 'text': 'Completed'
128
+ },
129
+ ScheduledMaintenance.Status.CANCELLED: {
130
+ 'emoji': '❌',
131
+ 'color': 'red',
132
+ 'text': 'Cancelled'
133
+ },
134
+ ScheduledMaintenance.Status.FAILED: {
135
+ 'emoji': '💥',
136
+ 'color': 'red',
137
+ 'text': 'Failed'
138
+ },
139
+ }
140
+
141
+ config = status_config.get(obj.status, {
142
+ 'emoji': '❓',
143
+ 'color': 'gray',
144
+ 'text': obj.get_status_display()
145
+ })
146
+
147
+ # Add timing info
148
+ timing_info = ""
149
+ if obj.status == ScheduledMaintenance.Status.SCHEDULED:
150
+ if obj.is_due:
151
+ timing_info = " <small>(Due now!)</small>"
152
+ elif obj.time_until_start:
153
+ hours = int(obj.time_until_start.total_seconds() // 3600)
154
+ if hours < 24:
155
+ timing_info = f" <small>(in {hours}h)</small>"
156
+ elif obj.status == ScheduledMaintenance.Status.ACTIVE:
157
+ if obj.is_overdue:
158
+ timing_info = " <small>(Overdue!)</small>"
159
+ elif obj.time_until_end:
160
+ hours = int(obj.time_until_end.total_seconds() // 3600)
161
+ minutes = int((obj.time_until_end.total_seconds() % 3600) // 60)
162
+ timing_info = f" <small>({hours}h {minutes}m left)</small>"
163
+
164
+ return format_html(
165
+ '<span style="color: {};">{} {}</span>{}',
166
+ config['color'],
167
+ config['emoji'],
168
+ config['text'],
169
+ timing_info
170
+ )
171
+
172
+ @display(description="Duration")
173
+ def duration_display(self, obj: ScheduledMaintenance) -> str:
174
+ """Display estimated vs actual duration."""
175
+ estimated_hours = obj.estimated_duration.total_seconds() / 3600
176
+
177
+ if obj.actual_duration:
178
+ actual_hours = obj.actual_duration.total_seconds() / 3600
179
+ return format_html(
180
+ '{:.1f}h <small>(actual: {:.1f}h)</small>',
181
+ estimated_hours,
182
+ actual_hours
183
+ )
184
+
185
+ return f"{estimated_hours:.1f}h"
186
+
187
+ @display(description="Sites")
188
+ def sites_count(self, obj: ScheduledMaintenance) -> str:
189
+ """Display sites count with link."""
190
+ count = obj.affected_sites_count
191
+ if count == 0:
192
+ return format_html('<span style="color: red;">No sites</span>')
193
+
194
+ return format_html(
195
+ '<span class="badge badge-info">{} sites</span>',
196
+ count
197
+ )
198
+
199
+ @display(description="Priority")
200
+ def priority_badge(self, obj: ScheduledMaintenance) -> str:
201
+ """Display priority badge."""
202
+ priority_config = {
203
+ 'low': {'color': 'green', 'emoji': '🟢'},
204
+ 'normal': {'color': 'blue', 'emoji': '🟡'},
205
+ 'high': {'color': 'orange', 'emoji': '🟠'},
206
+ 'critical': {'color': 'red', 'emoji': '🔴'},
207
+ }
208
+
209
+ config = priority_config.get(obj.priority, {'color': 'gray', 'emoji': '⚪'})
210
+
211
+ return format_html(
212
+ '<span style="color: {};">{} {}</span>',
213
+ config['color'],
214
+ config['emoji'],
215
+ obj.get_priority_display()
216
+ )
217
+
218
+ @display(description="Auto")
219
+ def auto_flags(self, obj: ScheduledMaintenance) -> str:
220
+ """Display automation flags."""
221
+ flags = []
222
+
223
+ if obj.auto_enable:
224
+ flags.append('<span style="color: green;">▶️ Start</span>')
225
+
226
+ if obj.auto_disable:
227
+ flags.append('<span style="color: blue;">⏹️ Stop</span>')
228
+
229
+ if not flags:
230
+ return '<span style="color: gray;">Manual</span>'
231
+
232
+ return format_html(' '.join(flags))
233
+
234
+ @action(description="Start Maintenance", variant=ActionVariant.SUCCESS)
235
+ def start_maintenance_action(self, request: HttpRequest, queryset: Any) -> None:
236
+ """Start selected maintenance events."""
237
+ started = 0
238
+ failed = 0
239
+
240
+ for maintenance in queryset:
241
+ if maintenance.status == ScheduledMaintenance.Status.SCHEDULED:
242
+ try:
243
+ result = maintenance.start_maintenance()
244
+ if result['success']:
245
+ started += 1
246
+ else:
247
+ failed += 1
248
+ except Exception as e:
249
+ failed += 1
250
+ messages.error(request, f"Failed to start {maintenance.title}: {e}")
251
+
252
+ if started > 0:
253
+ messages.success(request, f"Started {started} maintenance events")
254
+ if failed > 0:
255
+ messages.error(request, f"Failed to start {failed} maintenance events")
256
+
257
+ @action(description="Complete Maintenance", variant=ActionVariant.PRIMARY)
258
+ def complete_maintenance_action(self, request: HttpRequest, queryset: Any) -> None:
259
+ """Complete selected maintenance events."""
260
+ completed = 0
261
+ failed = 0
262
+
263
+ for maintenance in queryset:
264
+ if maintenance.status == ScheduledMaintenance.Status.ACTIVE:
265
+ try:
266
+ result = maintenance.complete_maintenance()
267
+ if result['success']:
268
+ completed += 1
269
+ else:
270
+ failed += 1
271
+ except Exception as e:
272
+ failed += 1
273
+ messages.error(request, f"Failed to complete {maintenance.title}: {e}")
274
+
275
+ if completed > 0:
276
+ messages.success(request, f"Completed {completed} maintenance events")
277
+ if failed > 0:
278
+ messages.error(request, f"Failed to complete {failed} maintenance events")
279
+
280
+ @action(description="Cancel Maintenance", variant=ActionVariant.DANGER)
281
+ def cancel_maintenance_action(self, request: HttpRequest, queryset: Any) -> None:
282
+ """Cancel selected maintenance events."""
283
+ cancelled = 0
284
+
285
+ for maintenance in queryset:
286
+ if maintenance.status in [ScheduledMaintenance.Status.SCHEDULED, ScheduledMaintenance.Status.ACTIVE]:
287
+ try:
288
+ result = maintenance.cancel_maintenance(reason="Cancelled via admin")
289
+ if result['success']:
290
+ cancelled += 1
291
+ except Exception as e:
292
+ messages.error(request, f"Failed to cancel {maintenance.title}: {e}")
293
+
294
+ if cancelled > 0:
295
+ messages.success(request, f"Cancelled {cancelled} maintenance events")
296
+
297
+ @action(description="Duplicate Maintenance")
298
+ def duplicate_maintenance_action(self, request: HttpRequest, queryset: Any) -> None:
299
+ """Duplicate selected maintenance events."""
300
+ duplicated = 0
301
+
302
+ for maintenance in queryset:
303
+ try:
304
+ # Create duplicate with new start time (1 week later)
305
+ new_start = maintenance.scheduled_start + timezone.timedelta(weeks=1)
306
+
307
+ duplicate = ScheduledMaintenance.objects.create(
308
+ title=f"{maintenance.title} (Copy)",
309
+ description=maintenance.description,
310
+ scheduled_start=new_start,
311
+ estimated_duration=maintenance.estimated_duration,
312
+ maintenance_message=maintenance.maintenance_message,
313
+ template=maintenance.template,
314
+ priority=maintenance.priority,
315
+ auto_enable=maintenance.auto_enable,
316
+ auto_disable=maintenance.auto_disable,
317
+ notify_before=maintenance.notify_before,
318
+ created_by=f"{maintenance.created_by} (duplicate)"
319
+ )
320
+
321
+ # Copy sites
322
+ duplicate.sites.set(maintenance.sites.all())
323
+ duplicated += 1
324
+
325
+ except Exception as e:
326
+ messages.error(request, f"Failed to duplicate {maintenance.title}: {e}")
327
+
328
+ if duplicated > 0:
329
+ messages.success(request, f"Duplicated {duplicated} maintenance events")
330
+
331
+ def calendar_view(self, request: HttpRequest) -> TemplateResponse:
332
+ """Calendar view for scheduled maintenances."""
333
+ from ..services.scheduled_maintenance_service import scheduled_maintenance_service
334
+
335
+ calendar_data = scheduled_maintenance_service.get_maintenance_calendar(days=30)
336
+
337
+ context = {
338
+ 'title': 'Maintenance Calendar',
339
+ 'calendar_data': calendar_data,
340
+ 'opts': self.model._meta,
341
+ }
342
+
343
+ return TemplateResponse(
344
+ request,
345
+ 'admin/maintenance/scheduled_maintenance_calendar.html',
346
+ context
347
+ )
348
+
349
+ def start_maintenance_view(self, request: HttpRequest, object_id: int) -> HttpResponse:
350
+ """Start specific maintenance event."""
351
+ maintenance = self.get_object(request, object_id)
352
+
353
+ if maintenance.status != ScheduledMaintenance.Status.SCHEDULED:
354
+ messages.error(request, f"Cannot start maintenance in {maintenance.status} status")
355
+ else:
356
+ try:
357
+ result = maintenance.start_maintenance()
358
+ if result['success']:
359
+ messages.success(
360
+ request,
361
+ f"Started maintenance '{maintenance.title}' affecting {result['sites_affected']} sites"
362
+ )
363
+ else:
364
+ messages.error(request, f"Failed to start maintenance: {result.get('error')}")
365
+ except Exception as e:
366
+ messages.error(request, f"Error starting maintenance: {e}")
367
+
368
+ return redirect('admin:maintenance_scheduledmaintenance_change', object_id)
369
+
370
+ def complete_maintenance_view(self, request: HttpRequest, object_id: int) -> HttpResponse:
371
+ """Complete specific maintenance event."""
372
+ maintenance = self.get_object(request, object_id)
373
+
374
+ if maintenance.status != ScheduledMaintenance.Status.ACTIVE:
375
+ messages.error(request, f"Cannot complete maintenance in {maintenance.status} status")
376
+ else:
377
+ try:
378
+ result = maintenance.complete_maintenance()
379
+ if result['success']:
380
+ duration = result.get('actual_duration', 0) / 3600
381
+ messages.success(
382
+ request,
383
+ f"Completed maintenance '{maintenance.title}' (duration: {duration:.1f}h)"
384
+ )
385
+ else:
386
+ messages.error(request, f"Failed to complete maintenance: {result.get('error')}")
387
+ except Exception as e:
388
+ messages.error(request, f"Error completing maintenance: {e}")
389
+
390
+ return redirect('admin:maintenance_scheduledmaintenance_change', object_id)