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.
Files changed (61) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/maintenance/README.md +305 -0
  3. django_cfg/apps/maintenance/__init__.py +27 -0
  4. django_cfg/apps/maintenance/admin/__init__.py +28 -0
  5. django_cfg/apps/maintenance/admin/deployments_admin.py +251 -0
  6. django_cfg/apps/maintenance/admin/events_admin.py +374 -0
  7. django_cfg/apps/maintenance/admin/monitoring_admin.py +215 -0
  8. django_cfg/apps/maintenance/admin/sites_admin.py +464 -0
  9. django_cfg/apps/maintenance/apps.py +105 -0
  10. django_cfg/apps/maintenance/management/__init__.py +0 -0
  11. django_cfg/apps/maintenance/management/commands/__init__.py +0 -0
  12. django_cfg/apps/maintenance/management/commands/maintenance.py +375 -0
  13. django_cfg/apps/maintenance/management/commands/sync_cloudflare.py +168 -0
  14. django_cfg/apps/maintenance/managers/__init__.py +20 -0
  15. django_cfg/apps/maintenance/managers/deployments.py +287 -0
  16. django_cfg/apps/maintenance/managers/events.py +374 -0
  17. django_cfg/apps/maintenance/managers/monitoring.py +301 -0
  18. django_cfg/apps/maintenance/managers/sites.py +335 -0
  19. django_cfg/apps/maintenance/migrations/0001_initial.py +939 -0
  20. django_cfg/apps/maintenance/migrations/__init__.py +0 -0
  21. django_cfg/apps/maintenance/models/__init__.py +27 -0
  22. django_cfg/apps/maintenance/models/cloudflare.py +316 -0
  23. django_cfg/apps/maintenance/models/maintenance.py +334 -0
  24. django_cfg/apps/maintenance/models/monitoring.py +393 -0
  25. django_cfg/apps/maintenance/models/sites.py +419 -0
  26. django_cfg/apps/maintenance/serializers/__init__.py +60 -0
  27. django_cfg/apps/maintenance/serializers/actions.py +310 -0
  28. django_cfg/apps/maintenance/serializers/base.py +44 -0
  29. django_cfg/apps/maintenance/serializers/deployments.py +209 -0
  30. django_cfg/apps/maintenance/serializers/events.py +210 -0
  31. django_cfg/apps/maintenance/serializers/monitoring.py +278 -0
  32. django_cfg/apps/maintenance/serializers/sites.py +213 -0
  33. django_cfg/apps/maintenance/services/README.md +168 -0
  34. django_cfg/apps/maintenance/services/__init__.py +21 -0
  35. django_cfg/apps/maintenance/services/cloudflare_client.py +441 -0
  36. django_cfg/apps/maintenance/services/dns_manager.py +497 -0
  37. django_cfg/apps/maintenance/services/maintenance_manager.py +504 -0
  38. django_cfg/apps/maintenance/services/site_sync.py +448 -0
  39. django_cfg/apps/maintenance/services/sync_command_service.py +330 -0
  40. django_cfg/apps/maintenance/services/worker_manager.py +264 -0
  41. django_cfg/apps/maintenance/signals.py +38 -0
  42. django_cfg/apps/maintenance/urls.py +36 -0
  43. django_cfg/apps/maintenance/views/__init__.py +18 -0
  44. django_cfg/apps/maintenance/views/base.py +61 -0
  45. django_cfg/apps/maintenance/views/deployments.py +175 -0
  46. django_cfg/apps/maintenance/views/events.py +204 -0
  47. django_cfg/apps/maintenance/views/monitoring.py +213 -0
  48. django_cfg/apps/maintenance/views/sites.py +338 -0
  49. django_cfg/apps/urls.py +5 -1
  50. django_cfg/core/config.py +34 -3
  51. django_cfg/core/generation.py +15 -10
  52. django_cfg/models/cloudflare.py +316 -0
  53. django_cfg/models/revolution.py +1 -1
  54. django_cfg/models/tasks.py +1 -1
  55. django_cfg/modules/base.py +12 -5
  56. django_cfg/modules/django_unfold/dashboard.py +16 -1
  57. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/METADATA +2 -1
  58. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/RECORD +61 -13
  59. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/WHEEL +0 -0
  60. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/entry_points.txt +0 -0
  61. {django_cfg-1.2.15.dist-info → django_cfg-1.2.17.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,338 @@
1
+ """
2
+ Site management views.
3
+
4
+ ViewSets for CloudflareSite and SiteGroup management.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime
9
+
10
+ from django.shortcuts import get_object_or_404
11
+ from rest_framework import viewsets, status
12
+ from rest_framework.decorators import action
13
+ from drf_spectacular.utils import (
14
+ extend_schema, extend_schema_view, OpenApiResponse, OpenApiParameter, OpenApiTypes
15
+ )
16
+
17
+ from ..models import CloudflareSite, SiteGroup
18
+ from ..serializers import (
19
+ CloudflareSiteSerializer, CloudflareSiteCreateSerializer, CloudflareSiteListSerializer,
20
+ SiteGroupSerializer, SiteGroupCreateSerializer,
21
+ BulkMaintenanceActionSerializer, APIResponseSerializer
22
+ )
23
+ from ..services import MaintenanceManager
24
+ from .base import MaintenancePermissionMixin, MaintenanceResponseMixin
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @extend_schema_view(
30
+ list=extend_schema(
31
+ summary="List Cloudflare sites",
32
+ description="Get list of Cloudflare sites with filtering options",
33
+ parameters=[
34
+ OpenApiParameter('environment', OpenApiTypes.STR, description='Filter by environment'),
35
+ OpenApiParameter('project', OpenApiTypes.STR, description='Filter by project'),
36
+ OpenApiParameter('status', OpenApiTypes.STR, description='Filter by status'),
37
+ OpenApiParameter('maintenance_active', OpenApiTypes.BOOL, description='Filter by maintenance status'),
38
+ OpenApiParameter('search', OpenApiTypes.STR, description='Search in name and domain'),
39
+ ]
40
+ ),
41
+ create=extend_schema(
42
+ summary="Create Cloudflare site",
43
+ description="Create a new Cloudflare site configuration"
44
+ ),
45
+ retrieve=extend_schema(
46
+ summary="Get Cloudflare site",
47
+ description="Get detailed information about a specific Cloudflare site"
48
+ ),
49
+ update=extend_schema(
50
+ summary="Update Cloudflare site",
51
+ description="Update Cloudflare site configuration"
52
+ ),
53
+ destroy=extend_schema(
54
+ summary="Delete Cloudflare site",
55
+ description="Delete a Cloudflare site configuration"
56
+ )
57
+ )
58
+ class CloudflareSiteViewSet(MaintenancePermissionMixin, MaintenanceResponseMixin, viewsets.ModelViewSet):
59
+ """ViewSet for managing Cloudflare sites."""
60
+
61
+ serializer_class = CloudflareSiteSerializer
62
+ lookup_field = 'id'
63
+ filterset_fields = ['environment', 'project', 'current_status', 'maintenance_active']
64
+ search_fields = ['name', 'domain', 'project']
65
+ ordering_fields = ['name', 'domain', 'created_at', 'last_maintenance_at']
66
+ ordering = ['-created_at']
67
+
68
+ def get_queryset(self):
69
+ """Get queryset filtered by user permissions."""
70
+ return self.get_user_queryset(CloudflareSite)
71
+
72
+ def get_serializer_class(self):
73
+ """Return appropriate serializer based on action."""
74
+ if self.action == 'create':
75
+ return CloudflareSiteCreateSerializer
76
+ elif self.action == 'list':
77
+ return CloudflareSiteListSerializer
78
+ elif self.action == 'bulk_action':
79
+ return BulkMaintenanceActionSerializer
80
+ return CloudflareSiteSerializer
81
+
82
+ def perform_create(self, serializer):
83
+ """Set owner when creating site."""
84
+ serializer.save(owner=self.request.user)
85
+
86
+ @action(detail=True, methods=['post'])
87
+ @extend_schema(
88
+ summary="Enable maintenance mode",
89
+ description="Enable maintenance mode for this site",
90
+ request=None,
91
+ responses={
92
+ 200: OpenApiResponse(response=APIResponseSerializer, description="Maintenance enabled successfully"),
93
+ 400: OpenApiResponse(response=APIResponseSerializer, description="Bad request"),
94
+ 500: OpenApiResponse(response=APIResponseSerializer, description="Internal server error")
95
+ }
96
+ )
97
+ def enable_maintenance(self, request, id=None):
98
+ """Enable maintenance mode for a site."""
99
+ try:
100
+ site = self.get_object()
101
+ reason = request.data.get('reason', 'Manual maintenance')
102
+ message = request.data.get('message', 'Site is under maintenance')
103
+
104
+ # Use multi-site manager for consistency
105
+ manager = MaintenanceManager(self.request.user)
106
+ success = manager._enable_site_maintenance(site, reason, message)
107
+
108
+ if success:
109
+ return self.success_response(f'Maintenance enabled for {site.domain}')
110
+ else:
111
+ return self.error_response('Failed to enable maintenance mode')
112
+
113
+ except Exception as e:
114
+ return self.error_response(f"Enable maintenance error for site {id}: {e}")
115
+
116
+ @action(detail=True, methods=['post'])
117
+ @extend_schema(
118
+ summary="Disable maintenance mode",
119
+ description="Disable maintenance mode for this site",
120
+ responses={
121
+ 200: OpenApiResponse(response=APIResponseSerializer, description="Maintenance disabled successfully"),
122
+ 500: OpenApiResponse(response=APIResponseSerializer, description="Internal server error")
123
+ }
124
+ )
125
+ def disable_maintenance(self, request, id=None):
126
+ """Disable maintenance mode for a site."""
127
+ try:
128
+ site = self.get_object()
129
+
130
+ # Use multi-site manager for consistency
131
+ manager = MaintenanceManager(self.request.user)
132
+ success = manager._disable_site_maintenance(site)
133
+
134
+ if success:
135
+ return self.success_response(f'Maintenance disabled for {site.domain}')
136
+ else:
137
+ return self.error_response('Failed to disable maintenance mode')
138
+
139
+ except Exception as e:
140
+ return self.error_response(f"Disable maintenance error for site {id}: {e}")
141
+
142
+ @action(detail=True, methods=['get'])
143
+ @extend_schema(
144
+ summary="Check site status",
145
+ description="Check current status of the site",
146
+ responses={
147
+ 200: OpenApiResponse(response=APIResponseSerializer, description="Status checked successfully")
148
+ }
149
+ )
150
+ def check_status(self, request, id=None):
151
+ """Check current status of a site."""
152
+ try:
153
+ site = self.get_object()
154
+
155
+ # Use multi-site manager for consistency
156
+ manager = MaintenanceManager(self.request.user)
157
+ site_status = manager._check_site_status(site)
158
+
159
+ return self.success_response(
160
+ 'Status checked successfully',
161
+ data={
162
+ 'site_id': site.id,
163
+ 'domain': site.domain,
164
+ 'status': site_status,
165
+ 'maintenance_active': site.maintenance_active,
166
+ 'last_check': datetime.now().isoformat()
167
+ }
168
+ )
169
+
170
+ except Exception as e:
171
+ return self.error_response(f"Status check error for site {id}: {e}")
172
+
173
+ @action(detail=False, methods=['post'])
174
+ @extend_schema(
175
+ summary="Bulk maintenance actions",
176
+ description="Perform bulk maintenance actions on multiple sites",
177
+ request=BulkMaintenanceActionSerializer,
178
+ responses={
179
+ 200: OpenApiResponse(response=APIResponseSerializer, description="Bulk action completed"),
180
+ 400: OpenApiResponse(response=APIResponseSerializer, description="Invalid request data")
181
+ }
182
+ )
183
+ def bulk_action(self, request):
184
+ """Perform bulk maintenance actions."""
185
+ serializer = BulkMaintenanceActionSerializer(data=request.data)
186
+ if not serializer.is_valid():
187
+ return self.validation_error_response(serializer.errors)
188
+
189
+ try:
190
+ action_type = serializer.validated_data['action']
191
+ site_ids = serializer.validated_data['site_ids']
192
+ dry_run = serializer.validated_data.get('dry_run', False)
193
+
194
+ # Get sites (filtered by user permissions)
195
+ sites = self.get_queryset().filter(id__in=site_ids)
196
+
197
+ if dry_run:
198
+ return self.success_response(
199
+ f'Dry run: would {action_type} maintenance for {sites.count()} sites',
200
+ data={
201
+ 'dry_run': True,
202
+ 'would_affect': [site.domain for site in sites]
203
+ }
204
+ )
205
+
206
+ # Use multi-site manager for bulk operations
207
+ manager = MaintenanceManager(self.request.user).sites(request.user).filter(id__in=site_ids)
208
+
209
+ if action_type == 'enable':
210
+ reason = serializer.validated_data.get('reason', 'Bulk maintenance')
211
+ result = manager.enable_maintenance(
212
+ reason=reason,
213
+ user=request.user,
214
+ message=serializer.validated_data.get('maintenance_message')
215
+ )
216
+ elif action_type == 'disable':
217
+ result = manager.disable_maintenance(user=request.user)
218
+ elif action_type == 'status_check':
219
+ result = manager.check_status()
220
+ else:
221
+ return self.error_response(f'Unknown action: {action_type}', status.HTTP_400_BAD_REQUEST)
222
+
223
+ return self.success_response(f'Bulk {action_type} completed', data=result)
224
+
225
+ except Exception as e:
226
+ return self.error_response(f"Bulk action error: {e}")
227
+
228
+
229
+ @extend_schema_view(
230
+ list=extend_schema(
231
+ summary="List site groups",
232
+ description="Get list of site groups"
233
+ ),
234
+ create=extend_schema(
235
+ summary="Create site group",
236
+ description="Create a new site group"
237
+ )
238
+ )
239
+ class SiteGroupViewSet(MaintenancePermissionMixin, MaintenanceResponseMixin, viewsets.ModelViewSet):
240
+ """ViewSet for managing site groups."""
241
+
242
+ serializer_class = SiteGroupSerializer
243
+ lookup_field = 'id'
244
+
245
+ def get_queryset(self):
246
+ """Get queryset filtered by user permissions."""
247
+ return self.get_user_queryset(SiteGroup)
248
+
249
+ def get_serializer_class(self):
250
+ """Return appropriate serializer based on action."""
251
+ if self.action == 'create':
252
+ return SiteGroupCreateSerializer
253
+ return SiteGroupSerializer
254
+
255
+ def perform_create(self, serializer):
256
+ """Set owner when creating group."""
257
+ serializer.save(owner=self.request.user)
258
+
259
+ @action(detail=True, methods=['post'])
260
+ @extend_schema(
261
+ summary="Add sites to group",
262
+ description="Add sites to this group"
263
+ )
264
+ def add_sites(self, request, id=None):
265
+ """Add sites to group."""
266
+ try:
267
+ group = self.get_object()
268
+ site_ids = request.data.get('site_ids', [])
269
+
270
+ # Get sites (filtered by user permissions)
271
+ sites = self.get_user_queryset(CloudflareSite).filter(id__in=site_ids)
272
+ group.sites.add(*sites)
273
+
274
+ return self.success_response(f'Added {sites.count()} sites to group {group.name}')
275
+
276
+ except Exception as e:
277
+ return self.error_response(f"Add sites to group error: {e}")
278
+
279
+ @action(detail=True, methods=['post'])
280
+ @extend_schema(
281
+ summary="Remove sites from group",
282
+ description="Remove sites from this group"
283
+ )
284
+ def remove_sites(self, request, id=None):
285
+ """Remove sites from group."""
286
+ try:
287
+ group = self.get_object()
288
+ site_ids = request.data.get('site_ids', [])
289
+
290
+ # Get sites (filtered by user permissions)
291
+ sites = self.get_user_queryset(CloudflareSite).filter(id__in=site_ids)
292
+ group.sites.remove(*sites)
293
+
294
+ return self.success_response(f'Removed {sites.count()} sites from group {group.name}')
295
+
296
+ except Exception as e:
297
+ return self.error_response(f"Remove sites from group error: {e}")
298
+
299
+ @action(detail=True, methods=['post'])
300
+ @extend_schema(
301
+ summary="Enable maintenance for group",
302
+ description="Enable maintenance mode for all sites in this group"
303
+ )
304
+ def enable_maintenance(self, request, id=None):
305
+ """Enable maintenance for all sites in group."""
306
+ try:
307
+ group = self.get_object()
308
+ reason = request.data.get('reason', f'Group maintenance: {group.name}')
309
+
310
+ result = group.enable_maintenance_for_all(request.user, reason)
311
+
312
+ return self.success_response(
313
+ f'Group maintenance enabled for {group.name}',
314
+ data=result
315
+ )
316
+
317
+ except Exception as e:
318
+ return self.error_response(f"Group maintenance enable error: {e}")
319
+
320
+ @action(detail=True, methods=['post'])
321
+ @extend_schema(
322
+ summary="Disable maintenance for group",
323
+ description="Disable maintenance mode for all sites in this group"
324
+ )
325
+ def disable_maintenance(self, request, id=None):
326
+ """Disable maintenance for all sites in group."""
327
+ try:
328
+ group = self.get_object()
329
+
330
+ result = group.disable_maintenance_for_all()
331
+
332
+ return self.success_response(
333
+ f'Group maintenance disabled for {group.name}',
334
+ data=result
335
+ )
336
+
337
+ except Exception as e:
338
+ return self.error_response(f"Group maintenance disable error: {e}")
django_cfg/apps/urls.py CHANGED
@@ -45,8 +45,12 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
45
45
  # patterns.append(path('leads/', include('django_cfg.apps.leads.urls')))
46
46
 
47
47
  # Tasks app - enabled when knowbase or agents are enabled
48
- if base_module.is_tasks_enabled():
48
+ if base_module.should_enable_tasks():
49
49
  patterns.append(path('tasks/', include('django_cfg.apps.tasks.urls')))
50
+
51
+ # Maintenance app - multi-site maintenance mode with Cloudflare
52
+ if base_module.is_maintenance_enabled():
53
+ patterns.append(path('maintenance/', include('django_cfg.apps.maintenance.urls')))
50
54
 
51
55
  except Exception:
52
56
  # Fallback: include all URLs if config is not available
django_cfg/core/config.py CHANGED
@@ -58,7 +58,7 @@ DEFAULT_APPS = [
58
58
  "django_extensions",
59
59
  "constance",
60
60
  "constance.backends.database",
61
- "django_dramatiq",
61
+ # Note: django_dramatiq is added conditionally when tasks are enabled
62
62
  # Django CFG
63
63
  "django_cfg",
64
64
  "django_revolution",
@@ -270,6 +270,12 @@ class DjangoConfig(BaseModel):
270
270
  description="Application limits configuration (file uploads, requests, etc.)",
271
271
  )
272
272
 
273
+ # === Maintenance Mode Configuration ===
274
+ enable_maintenance: bool = Field(
275
+ default=False,
276
+ description="Enable django-cfg Maintenance application (multi-site maintenance mode with Cloudflare)",
277
+ )
278
+
273
279
  # === Middleware Configuration ===
274
280
  custom_middleware: List[str] = Field(
275
281
  default_factory=list,
@@ -518,6 +524,28 @@ class DjangoConfig(BaseModel):
518
524
  code=otp_code
519
525
  )
520
526
 
527
+ def should_enable_tasks(self) -> bool:
528
+ """
529
+ Determine if background tasks should be enabled.
530
+
531
+ Tasks are enabled if:
532
+ 1. Explicitly configured via tasks field
533
+ 2. Knowledge base is enabled (requires background processing)
534
+ 3. Agents are enabled (requires background processing)
535
+
536
+ Returns:
537
+ True if tasks should be enabled, False otherwise
538
+ """
539
+ # Check if explicitly configured
540
+ if hasattr(self, 'tasks') and self.tasks and self.tasks.enabled:
541
+ return True
542
+
543
+ # Check if features that require tasks are enabled
544
+ if self.enable_knowbase or self.enable_agents:
545
+ return True
546
+
547
+ return False
548
+
521
549
  def get_installed_apps(self) -> List[str]:
522
550
  """
523
551
  Get complete list of installed Django apps.
@@ -547,9 +575,12 @@ class DjangoConfig(BaseModel):
547
575
  apps.append("django_cfg.apps.knowbase")
548
576
  if self.enable_agents:
549
577
  apps.append("django_cfg.apps.agents")
578
+ if self.enable_maintenance:
579
+ apps.append("django_cfg.apps.maintenance")
550
580
 
551
- # Auto-enable tasks if knowbase or agents are enabled
552
- if self.enable_knowbase or self.enable_agents:
581
+ # Auto-enable tasks if needed
582
+ if self.should_enable_tasks():
583
+ apps.append("django_dramatiq") # Add django_dramatiq first
553
584
  apps.append("django_cfg.apps.tasks")
554
585
 
555
586
  # Auto-detect dashboard apps from Unfold callback
@@ -418,16 +418,21 @@ class SettingsGenerator:
418
418
 
419
419
  # Check for Tasks/Dramatiq configuration
420
420
  try:
421
- from django_cfg.models.tasks import TaskConfig
422
- from django_cfg.modules.django_tasks import generate_dramatiq_settings_from_config
423
-
424
- # Auto-initialize TaskConfig if needed and generate settings
425
- task_config = TaskConfig.auto_initialize_if_needed()
426
- if task_config is not None:
427
- dramatiq_settings = generate_dramatiq_settings_from_config()
428
- if dramatiq_settings:
429
- settings.update(dramatiq_settings)
430
- integrations.append("dramatiq")
421
+ # Use the config's computed method to determine if tasks should be enabled
422
+ if config.should_enable_tasks():
423
+ from django_cfg.models.tasks import TaskConfig
424
+ from django_cfg.modules.django_tasks import generate_dramatiq_settings_from_config
425
+
426
+ # Auto-initialize TaskConfig if needed and generate settings
427
+ task_config = TaskConfig.auto_initialize_if_needed()
428
+ if task_config is not None:
429
+ dramatiq_settings = generate_dramatiq_settings_from_config()
430
+ if dramatiq_settings:
431
+ settings.update(dramatiq_settings)
432
+ integrations.append("dramatiq")
433
+ logger.info("✅ Dramatiq enabled (tasks/knowbase/agents required)")
434
+ else:
435
+ logger.debug("⏭️ Dramatiq disabled (no tasks/knowbase/agents)")
431
436
 
432
437
  except ImportError as e:
433
438
  logger.warning(f"Failed to import django_tasks module: {e}")