django-cfg 1.5.8__py3-none-any.whl → 1.5.20__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +126 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -85,8 +85,9 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
|
85
85
|
"total_examples": len(examples),
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
serializer = GRPCExamplesListSerializer(
|
|
89
|
-
|
|
88
|
+
serializer = GRPCExamplesListSerializer(data=response_data)
|
|
89
|
+
serializer.is_valid(raise_exception=True)
|
|
90
|
+
return Response(serializer.data)
|
|
90
91
|
|
|
91
92
|
except Exception as e:
|
|
92
93
|
logger.error(f"Examples fetch error: {e}", exc_info=True)
|
|
@@ -147,35 +148,33 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
|
147
148
|
# Serialize paginated data
|
|
148
149
|
logs_list = []
|
|
149
150
|
for log in page:
|
|
150
|
-
|
|
151
|
-
request_id
|
|
152
|
-
service
|
|
153
|
-
method
|
|
154
|
-
status
|
|
155
|
-
grpc_status_code
|
|
156
|
-
error_message
|
|
157
|
-
duration_ms
|
|
158
|
-
created_at
|
|
159
|
-
user
|
|
160
|
-
)
|
|
161
|
-
logs_list.append(log_serializer.model_dump())
|
|
151
|
+
logs_list.append({
|
|
152
|
+
"request_id": log.request_id,
|
|
153
|
+
"service": log.service_name,
|
|
154
|
+
"method": log.method_name,
|
|
155
|
+
"status": log.status,
|
|
156
|
+
"grpc_status_code": log.grpc_status_code or "",
|
|
157
|
+
"error_message": log.error_message or "",
|
|
158
|
+
"duration_ms": log.duration_ms or 0,
|
|
159
|
+
"created_at": log.created_at.isoformat(),
|
|
160
|
+
"user": log.user.username if log.user else None,
|
|
161
|
+
})
|
|
162
162
|
return self.get_paginated_response(logs_list)
|
|
163
163
|
|
|
164
164
|
# No pagination (shouldn't happen with default pagination)
|
|
165
165
|
logs_list = []
|
|
166
166
|
for log in queryset[:100]: # Safety limit
|
|
167
|
-
|
|
168
|
-
request_id
|
|
169
|
-
service
|
|
170
|
-
method
|
|
171
|
-
status
|
|
172
|
-
grpc_status_code
|
|
173
|
-
error_message
|
|
174
|
-
duration_ms
|
|
175
|
-
created_at
|
|
176
|
-
user
|
|
177
|
-
)
|
|
178
|
-
logs_list.append(log_serializer.model_dump())
|
|
167
|
+
logs_list.append({
|
|
168
|
+
"request_id": log.request_id,
|
|
169
|
+
"service": log.service_name,
|
|
170
|
+
"method": log.method_name,
|
|
171
|
+
"status": log.status,
|
|
172
|
+
"grpc_status_code": log.grpc_status_code or "",
|
|
173
|
+
"error_message": log.error_message or "",
|
|
174
|
+
"duration_ms": log.duration_ms or 0,
|
|
175
|
+
"created_at": log.created_at.isoformat(),
|
|
176
|
+
"user": log.user.username if log.user else None,
|
|
177
|
+
})
|
|
179
178
|
return Response({"logs": logs_list, "count": len(logs_list)})
|
|
180
179
|
|
|
181
180
|
except Exception as e:
|
|
@@ -221,9 +220,10 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
|
221
220
|
from django.utils import timezone
|
|
222
221
|
|
|
223
222
|
try:
|
|
224
|
-
# Validate request -
|
|
225
|
-
serializer = GRPCCallRequestSerializer(
|
|
226
|
-
|
|
223
|
+
# Validate request - DRF validation
|
|
224
|
+
serializer = GRPCCallRequestSerializer(data=request.data)
|
|
225
|
+
serializer.is_valid(raise_exception=True)
|
|
226
|
+
data = serializer.validated_data
|
|
227
227
|
|
|
228
228
|
service_name = data["service"]
|
|
229
229
|
method_name = data["method"]
|
|
@@ -275,21 +275,23 @@ class GRPCTestingViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
|
275
275
|
if response_data and isinstance(response_data, dict):
|
|
276
276
|
response_data = json.dumps(response_data)
|
|
277
277
|
|
|
278
|
-
|
|
279
|
-
success
|
|
280
|
-
request_id
|
|
281
|
-
service
|
|
282
|
-
method
|
|
283
|
-
status
|
|
284
|
-
grpc_status_code
|
|
285
|
-
duration_ms
|
|
286
|
-
response
|
|
287
|
-
error
|
|
288
|
-
metadata
|
|
289
|
-
timestamp
|
|
290
|
-
|
|
278
|
+
response_data_dict = {
|
|
279
|
+
"success": result["success"],
|
|
280
|
+
"request_id": request_id,
|
|
281
|
+
"service": result["service"],
|
|
282
|
+
"method": result["method"],
|
|
283
|
+
"status": log.status,
|
|
284
|
+
"grpc_status_code": result["grpc_status_code"],
|
|
285
|
+
"duration_ms": result["duration_ms"],
|
|
286
|
+
"response": response_data,
|
|
287
|
+
"error": result.get("error_message"),
|
|
288
|
+
"metadata": {},
|
|
289
|
+
"timestamp": end_time.isoformat(),
|
|
290
|
+
}
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
response_serializer = GRPCCallResponseSerializer(data=response_data_dict)
|
|
293
|
+
response_serializer.is_valid(raise_exception=True)
|
|
294
|
+
return Response(response_serializer.data, status=status.HTTP_200_OK)
|
|
293
295
|
|
|
294
296
|
except Exception as e:
|
|
295
297
|
logger.error(f"gRPC call endpoint error: {e}", exc_info=True)
|
|
@@ -19,7 +19,7 @@ from ..services import job_to_model
|
|
|
19
19
|
logger = get_logger("rq.jobs")
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class JobViewSet(AdminAPIMixin, viewsets.
|
|
22
|
+
class JobViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
23
23
|
"""
|
|
24
24
|
ViewSet for RQ job management.
|
|
25
25
|
|
|
@@ -33,6 +33,8 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
33
33
|
Requires admin authentication (JWT, Session, or Basic Auth).
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
+
serializer_class = JobListSerializer
|
|
37
|
+
|
|
36
38
|
@extend_schema(
|
|
37
39
|
tags=["RQ Jobs"],
|
|
38
40
|
summary="List all jobs",
|
|
@@ -455,9 +457,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
455
457
|
except Exception as e:
|
|
456
458
|
logger.debug(f"Failed to get failed jobs for queue {queue_name}: {e}")
|
|
457
459
|
|
|
458
|
-
|
|
460
|
+
# Use DRF pagination
|
|
461
|
+
page = self.paginate_queryset(all_jobs)
|
|
462
|
+
serializer = JobListSerializer(data=page, many=True)
|
|
459
463
|
serializer.is_valid(raise_exception=True)
|
|
460
|
-
return
|
|
464
|
+
return self.get_paginated_response(serializer.data)
|
|
461
465
|
|
|
462
466
|
except Exception as e:
|
|
463
467
|
logger.error(f"Failed jobs list error: {e}", exc_info=True)
|
|
@@ -530,9 +534,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
530
534
|
except Exception as e:
|
|
531
535
|
logger.debug(f"Failed to get finished jobs for queue {queue_name}: {e}")
|
|
532
536
|
|
|
533
|
-
|
|
537
|
+
# Use DRF pagination
|
|
538
|
+
page = self.paginate_queryset(all_jobs)
|
|
539
|
+
serializer = JobListSerializer(data=page, many=True)
|
|
534
540
|
serializer.is_valid(raise_exception=True)
|
|
535
|
-
return
|
|
541
|
+
return self.get_paginated_response(serializer.data)
|
|
536
542
|
|
|
537
543
|
except Exception as e:
|
|
538
544
|
logger.error(f"Finished jobs list error: {e}", exc_info=True)
|
|
@@ -795,9 +801,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
795
801
|
except Exception as e:
|
|
796
802
|
logger.debug(f"Failed to get deferred jobs for queue {queue_name}: {e}")
|
|
797
803
|
|
|
798
|
-
|
|
804
|
+
# Use DRF pagination
|
|
805
|
+
page = self.paginate_queryset(all_jobs)
|
|
806
|
+
serializer = JobListSerializer(data=page, many=True)
|
|
799
807
|
serializer.is_valid(raise_exception=True)
|
|
800
|
-
return
|
|
808
|
+
return self.get_paginated_response(serializer.data)
|
|
801
809
|
|
|
802
810
|
except Exception as e:
|
|
803
811
|
logger.error(f"Deferred jobs list error: {e}", exc_info=True)
|
|
@@ -870,9 +878,11 @@ class JobViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
870
878
|
except Exception as e:
|
|
871
879
|
logger.debug(f"Failed to get started jobs for queue {queue_name}: {e}")
|
|
872
880
|
|
|
873
|
-
|
|
881
|
+
# Use DRF pagination
|
|
882
|
+
page = self.paginate_queryset(all_jobs)
|
|
883
|
+
serializer = JobListSerializer(data=page, many=True)
|
|
874
884
|
serializer.is_valid(raise_exception=True)
|
|
875
|
-
return
|
|
885
|
+
return self.get_paginated_response(serializer.data)
|
|
876
886
|
|
|
877
887
|
except Exception as e:
|
|
878
888
|
logger.error(f"Started jobs list error: {e}", exc_info=True)
|
|
@@ -23,7 +23,7 @@ from ..serializers import (
|
|
|
23
23
|
logger = get_logger("rq.schedule")
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class ScheduleViewSet(AdminAPIMixin, viewsets.
|
|
26
|
+
class ScheduleViewSet(AdminAPIMixin, viewsets.GenericViewSet):
|
|
27
27
|
"""
|
|
28
28
|
ViewSet for RQ schedule management.
|
|
29
29
|
|
|
@@ -37,6 +37,8 @@ class ScheduleViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
37
37
|
Requires rq-scheduler to be installed: pip install rq-scheduler
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
+
serializer_class = ScheduledJobSerializer
|
|
41
|
+
|
|
40
42
|
@extend_schema(
|
|
41
43
|
tags=["RQ Schedules"],
|
|
42
44
|
summary="List scheduled jobs",
|
|
@@ -131,9 +133,11 @@ class ScheduleViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
|
131
133
|
}
|
|
132
134
|
all_jobs.append(job_data)
|
|
133
135
|
|
|
134
|
-
|
|
136
|
+
# Use DRF pagination
|
|
137
|
+
page = self.paginate_queryset(all_jobs)
|
|
138
|
+
serializer = ScheduledJobSerializer(data=page, many=True)
|
|
135
139
|
serializer.is_valid(raise_exception=True)
|
|
136
|
-
return
|
|
140
|
+
return self.get_paginated_response(serializer.data)
|
|
137
141
|
|
|
138
142
|
except Exception as e:
|
|
139
143
|
logger.error(f"Failed to list scheduled jobs: {e}", exc_info=True)
|
|
@@ -8,7 +8,14 @@ from rest_framework import serializers
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class CommandSerializer(serializers.Serializer):
|
|
11
|
-
"""
|
|
11
|
+
"""
|
|
12
|
+
Django management command serializer.
|
|
13
|
+
|
|
14
|
+
Includes security metadata from base classes (SafeCommand, InteractiveCommand, etc.):
|
|
15
|
+
- web_executable: Can be executed via web interface
|
|
16
|
+
- requires_input: Requires interactive user input
|
|
17
|
+
- is_destructive: Modifies or deletes data
|
|
18
|
+
"""
|
|
12
19
|
name = serializers.CharField()
|
|
13
20
|
app = serializers.CharField()
|
|
14
21
|
help = serializers.CharField()
|
|
@@ -17,6 +24,23 @@ class CommandSerializer(serializers.Serializer):
|
|
|
17
24
|
is_allowed = serializers.BooleanField(required=False)
|
|
18
25
|
risk_level = serializers.CharField(required=False)
|
|
19
26
|
|
|
27
|
+
# Security metadata from command base classes
|
|
28
|
+
web_executable = serializers.BooleanField(
|
|
29
|
+
required=False,
|
|
30
|
+
allow_null=True,
|
|
31
|
+
help_text="Can be executed via web interface"
|
|
32
|
+
)
|
|
33
|
+
requires_input = serializers.BooleanField(
|
|
34
|
+
required=False,
|
|
35
|
+
allow_null=True,
|
|
36
|
+
help_text="Requires interactive user input"
|
|
37
|
+
)
|
|
38
|
+
is_destructive = serializers.BooleanField(
|
|
39
|
+
required=False,
|
|
40
|
+
allow_null=True,
|
|
41
|
+
help_text="Modifies or deletes data"
|
|
42
|
+
)
|
|
43
|
+
|
|
20
44
|
|
|
21
45
|
class CommandsSummarySerializer(serializers.Serializer):
|
|
22
46
|
"""Commands summary serializer."""
|
|
@@ -5,6 +5,8 @@ Serializers for displaying user's DjangoConfig settings.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from rest_framework import serializers
|
|
8
|
+
from drf_spectacular.utils import extend_schema_field
|
|
9
|
+
from drf_spectacular.types import OpenApiTypes
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
# Nested serializers for complex structures
|
|
@@ -89,6 +91,18 @@ class RedisQueueConfigSerializer(serializers.Serializer):
|
|
|
89
91
|
socket_timeout = serializers.IntegerField(required=False, allow_null=True)
|
|
90
92
|
|
|
91
93
|
|
|
94
|
+
class RQScheduleSerializer(serializers.Serializer):
|
|
95
|
+
"""Redis Queue schedule configuration."""
|
|
96
|
+
func = serializers.CharField(required=False, allow_null=True)
|
|
97
|
+
cron_string = serializers.CharField(required=False, allow_null=True)
|
|
98
|
+
queue = serializers.CharField(required=False, allow_null=True)
|
|
99
|
+
kwargs = serializers.DictField(required=False, allow_null=True)
|
|
100
|
+
args = serializers.ListField(required=False, allow_null=True)
|
|
101
|
+
meta = serializers.DictField(required=False, allow_null=True)
|
|
102
|
+
repeat = serializers.IntegerField(required=False, allow_null=True)
|
|
103
|
+
result_ttl = serializers.IntegerField(required=False, allow_null=True)
|
|
104
|
+
|
|
105
|
+
|
|
92
106
|
class DjangoRQConfigSerializer(serializers.Serializer):
|
|
93
107
|
"""Django-RQ configuration."""
|
|
94
108
|
enabled = serializers.BooleanField(required=False, allow_null=True)
|
|
@@ -97,7 +111,7 @@ class DjangoRQConfigSerializer(serializers.Serializer):
|
|
|
97
111
|
exception_handlers = serializers.ListField(required=False, allow_null=True)
|
|
98
112
|
api_token = serializers.CharField(required=False, allow_null=True)
|
|
99
113
|
prometheus_enabled = serializers.BooleanField(required=False, allow_null=True)
|
|
100
|
-
schedules = serializers.ListField(child=
|
|
114
|
+
schedules = serializers.ListField(child=RQScheduleSerializer(), required=False, allow_null=True)
|
|
101
115
|
|
|
102
116
|
|
|
103
117
|
class DRFConfigSerializer(serializers.Serializer):
|
|
@@ -126,6 +140,78 @@ class ConfigMetaSerializer(serializers.Serializer):
|
|
|
126
140
|
secret_key_configured = serializers.BooleanField()
|
|
127
141
|
|
|
128
142
|
|
|
143
|
+
class TelegramConfigSerializer(serializers.Serializer):
|
|
144
|
+
"""Telegram service configuration."""
|
|
145
|
+
bot_token = serializers.CharField(required=False, allow_null=True)
|
|
146
|
+
chat_id = serializers.IntegerField(required=False, allow_null=True)
|
|
147
|
+
parse_mode = serializers.CharField(required=False, allow_null=True)
|
|
148
|
+
disable_notification = serializers.BooleanField(required=False, allow_null=True)
|
|
149
|
+
disable_web_page_preview = serializers.BooleanField(required=False, allow_null=True)
|
|
150
|
+
timeout = serializers.IntegerField(required=False, allow_null=True)
|
|
151
|
+
webhook_url = serializers.CharField(required=False, allow_null=True)
|
|
152
|
+
webhook_secret = serializers.CharField(required=False, allow_null=True)
|
|
153
|
+
max_retries = serializers.IntegerField(required=False, allow_null=True)
|
|
154
|
+
retry_delay = serializers.FloatField(required=False, allow_null=True)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class NgrokConfigSerializer(serializers.Serializer):
|
|
158
|
+
"""Ngrok tunneling configuration."""
|
|
159
|
+
enabled = serializers.BooleanField(required=False, allow_null=True)
|
|
160
|
+
authtoken = serializers.CharField(required=False, allow_null=True)
|
|
161
|
+
basic_auth = serializers.ListField(required=False, allow_null=True)
|
|
162
|
+
compression = serializers.BooleanField(required=False, allow_null=True)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AxesConfigSerializer(serializers.Serializer):
|
|
166
|
+
"""Django-Axes brute-force protection configuration."""
|
|
167
|
+
enabled = serializers.BooleanField(required=False, allow_null=True)
|
|
168
|
+
failure_limit = serializers.IntegerField(required=False, allow_null=True)
|
|
169
|
+
cooloff_time = serializers.IntegerField(required=False, allow_null=True)
|
|
170
|
+
lock_out_at_failure = serializers.BooleanField(required=False, allow_null=True)
|
|
171
|
+
reset_on_success = serializers.BooleanField(required=False, allow_null=True)
|
|
172
|
+
only_user_failures = serializers.BooleanField(required=False, allow_null=True)
|
|
173
|
+
lockout_template = serializers.CharField(required=False, allow_null=True)
|
|
174
|
+
lockout_url = serializers.CharField(required=False, allow_null=True)
|
|
175
|
+
verbose = serializers.BooleanField(required=False, allow_null=True)
|
|
176
|
+
enable_access_failure_log = serializers.BooleanField(required=False, allow_null=True)
|
|
177
|
+
ipware_proxy_count = serializers.IntegerField(required=False, allow_null=True)
|
|
178
|
+
ipware_meta_precedence_order = serializers.ListField(required=False, allow_null=True)
|
|
179
|
+
allowed_ips = serializers.ListField(required=False, allow_null=True)
|
|
180
|
+
denied_ips = serializers.ListField(required=False, allow_null=True)
|
|
181
|
+
cache_name = serializers.CharField(required=False, allow_null=True)
|
|
182
|
+
use_user_agent = serializers.BooleanField(required=False, allow_null=True)
|
|
183
|
+
username_form_field = serializers.CharField(required=False, allow_null=True)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class NextJSAdminConfigSerializer(serializers.Serializer):
|
|
187
|
+
"""Next.js Admin application configuration."""
|
|
188
|
+
enabled = serializers.BooleanField(required=False, allow_null=True)
|
|
189
|
+
url = serializers.CharField(required=False, allow_null=True)
|
|
190
|
+
api_base_url = serializers.CharField(required=False, allow_null=True)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ConstanceConfigSerializer(serializers.Serializer):
|
|
194
|
+
"""Django-Constance dynamic settings configuration."""
|
|
195
|
+
config = serializers.DictField(required=False, allow_null=True)
|
|
196
|
+
config_fieldsets = serializers.DictField(required=False, allow_null=True)
|
|
197
|
+
backend = serializers.CharField(required=False, allow_null=True)
|
|
198
|
+
database_prefix = serializers.CharField(required=False, allow_null=True)
|
|
199
|
+
database_cache_backend = serializers.CharField(required=False, allow_null=True)
|
|
200
|
+
additional_config = serializers.DictField(required=False, allow_null=True)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class OpenAPIClientConfigSerializer(serializers.Serializer):
|
|
204
|
+
"""OpenAPI Client generation configuration."""
|
|
205
|
+
enabled = serializers.BooleanField(required=False, allow_null=True)
|
|
206
|
+
output_dir = serializers.CharField(required=False, allow_null=True)
|
|
207
|
+
client_name = serializers.CharField(required=False, allow_null=True)
|
|
208
|
+
schema_url = serializers.CharField(required=False, allow_null=True)
|
|
209
|
+
generator = serializers.CharField(required=False, allow_null=True)
|
|
210
|
+
additional_properties = serializers.DictField(required=False, allow_null=True)
|
|
211
|
+
templates = serializers.ListField(required=False, allow_null=True)
|
|
212
|
+
global_properties = serializers.DictField(required=False, allow_null=True)
|
|
213
|
+
|
|
214
|
+
|
|
129
215
|
class DjangoConfigSerializer(serializers.Serializer):
|
|
130
216
|
"""
|
|
131
217
|
Typed serializer for user's DjangoConfig settings.
|
|
@@ -188,10 +274,10 @@ class DjangoConfigSerializer(serializers.Serializer):
|
|
|
188
274
|
spectacular = SpectacularConfigSerializer(required=False, allow_null=True)
|
|
189
275
|
jwt = JWTConfigSerializer(required=False, allow_null=True)
|
|
190
276
|
|
|
191
|
-
#
|
|
192
|
-
telegram =
|
|
193
|
-
ngrok =
|
|
194
|
-
axes =
|
|
277
|
+
# Services & Security (now typed!)
|
|
278
|
+
telegram = TelegramConfigSerializer(required=False, allow_null=True)
|
|
279
|
+
ngrok = NgrokConfigSerializer(required=False, allow_null=True)
|
|
280
|
+
axes = AxesConfigSerializer(required=False, allow_null=True)
|
|
195
281
|
crypto_fields = serializers.DictField(required=False, allow_null=True)
|
|
196
282
|
unfold = serializers.CharField(required=False, allow_null=True) # String representation of Unfold config
|
|
197
283
|
tailwind_app_name = serializers.CharField(required=False, allow_null=True)
|
|
@@ -199,10 +285,10 @@ class DjangoConfigSerializer(serializers.Serializer):
|
|
|
199
285
|
limits = serializers.DictField(required=False, allow_null=True)
|
|
200
286
|
api_keys = serializers.DictField(required=False, allow_null=True)
|
|
201
287
|
custom_middleware = serializers.ListField(required=False, allow_null=True)
|
|
202
|
-
nextjs_admin =
|
|
288
|
+
nextjs_admin = NextJSAdminConfigSerializer(required=False, allow_null=True)
|
|
203
289
|
admin_emails = serializers.ListField(required=False, allow_null=True)
|
|
204
|
-
constance =
|
|
205
|
-
openapi_client =
|
|
290
|
+
constance = ConstanceConfigSerializer(required=False, allow_null=True)
|
|
291
|
+
openapi_client = OpenAPIClientConfigSerializer(required=False, allow_null=True)
|
|
206
292
|
|
|
207
293
|
# Metadata
|
|
208
294
|
_meta = ConfigMetaSerializer(required=False, allow_null=True)
|
|
@@ -238,7 +324,7 @@ class ConfigDataSerializer(serializers.Serializer):
|
|
|
238
324
|
help_text="User's DjangoConfig settings"
|
|
239
325
|
)
|
|
240
326
|
django_settings = serializers.DictField(
|
|
241
|
-
help_text="Complete Django settings (sanitized)"
|
|
327
|
+
help_text="Complete Django settings (sanitized, contains mixed types)"
|
|
242
328
|
)
|
|
243
329
|
_validation = ConfigValidationSerializer(
|
|
244
330
|
help_text="Validation result comparing serializer with actual config"
|
|
@@ -36,11 +36,16 @@ class UserStatisticsSerializer(serializers.Serializer):
|
|
|
36
36
|
superusers = serializers.IntegerField(help_text="Number of superusers")
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
class AppStatisticsDataSerializer(serializers.Serializer):
|
|
40
|
+
"""Serializer for application statistics data."""
|
|
41
|
+
|
|
42
|
+
name = serializers.CharField(help_text="Human-readable app name")
|
|
43
|
+
total_records = serializers.IntegerField(help_text="Total records count")
|
|
44
|
+
model_count = serializers.IntegerField(help_text="Number of models")
|
|
45
|
+
|
|
46
|
+
|
|
39
47
|
class AppStatisticsSerializer(serializers.Serializer):
|
|
40
48
|
"""Serializer for application-specific statistics."""
|
|
41
49
|
|
|
42
50
|
app_name = serializers.CharField(help_text="Application name")
|
|
43
|
-
statistics =
|
|
44
|
-
child=serializers.IntegerField(),
|
|
45
|
-
help_text="Application statistics"
|
|
46
|
-
)
|
|
51
|
+
statistics = AppStatisticsDataSerializer(help_text="Application statistics")
|
|
@@ -55,7 +55,7 @@ class CommandsService:
|
|
|
55
55
|
continue
|
|
56
56
|
|
|
57
57
|
try:
|
|
58
|
-
# Try to load command to get help text
|
|
58
|
+
# Try to load command to get help text and metadata
|
|
59
59
|
command = load_command_class(app_name, command_name)
|
|
60
60
|
help_text = getattr(command, 'help', 'No description available')
|
|
61
61
|
|
|
@@ -66,6 +66,11 @@ class CommandsService:
|
|
|
66
66
|
# Get risk level
|
|
67
67
|
risk_level = get_command_risk_level(command_name, app_name)
|
|
68
68
|
|
|
69
|
+
# Extract security metadata from command instance
|
|
70
|
+
web_executable = getattr(command, 'web_executable', None)
|
|
71
|
+
requires_input = getattr(command, 'requires_input', None)
|
|
72
|
+
is_destructive = getattr(command, 'is_destructive', None)
|
|
73
|
+
|
|
69
74
|
commands_list.append({
|
|
70
75
|
'name': command_name,
|
|
71
76
|
'app': app_name,
|
|
@@ -74,6 +79,9 @@ class CommandsService:
|
|
|
74
79
|
'is_custom': is_custom,
|
|
75
80
|
'is_allowed': is_allowed,
|
|
76
81
|
'risk_level': risk_level,
|
|
82
|
+
'web_executable': web_executable,
|
|
83
|
+
'requires_input': requires_input,
|
|
84
|
+
'is_destructive': is_destructive,
|
|
77
85
|
})
|
|
78
86
|
except Exception as e:
|
|
79
87
|
# If we can't load the command, still include basic info
|
|
@@ -86,6 +94,9 @@ class CommandsService:
|
|
|
86
94
|
'is_custom': app_name == 'django_cfg',
|
|
87
95
|
'is_allowed': is_allowed,
|
|
88
96
|
'risk_level': get_command_risk_level(command_name, app_name),
|
|
97
|
+
'web_executable': None,
|
|
98
|
+
'requires_input': None,
|
|
99
|
+
'is_destructive': None,
|
|
89
100
|
})
|
|
90
101
|
|
|
91
102
|
# Sort by name
|
|
@@ -130,17 +130,23 @@ class ZipExtractionMixin:
|
|
|
130
130
|
@method_decorator(xframe_options_exempt, name='dispatch')
|
|
131
131
|
class NextJSStaticView(ZipExtractionMixin, View):
|
|
132
132
|
"""
|
|
133
|
-
Serve Next.js static build files with automatic JWT token injection.
|
|
133
|
+
Serve Next.js static build files with automatic JWT token injection and precompression support.
|
|
134
134
|
|
|
135
135
|
Features:
|
|
136
136
|
- Serves Next.js static export files like a static file server
|
|
137
137
|
- Smart ZIP extraction: compares ZIP metadata (size + mtime) with marker file
|
|
138
|
-
- Automatically injects JWT tokens for authenticated users
|
|
139
|
-
-
|
|
138
|
+
- Automatically injects JWT tokens for authenticated users (HTML only)
|
|
139
|
+
- **Precompression support**: Automatically serves .br or .gz files if available
|
|
140
140
|
- Handles Next.js client-side routing (.html fallback)
|
|
141
141
|
- Automatically serves index.html for directory paths
|
|
142
142
|
- X-Frame-Options exempt to allow embedding in iframes
|
|
143
143
|
|
|
144
|
+
Compression Strategy:
|
|
145
|
+
- Brotli (.br) preferred over Gzip (.gz) - ~5-15% better compression
|
|
146
|
+
- Automatically detects browser support via Accept-Encoding header
|
|
147
|
+
- Skips compression for HTML files (JWT injection requires uncompressed content)
|
|
148
|
+
- Only serves precompressed files, no runtime compression
|
|
149
|
+
|
|
144
150
|
ZIP Extraction Logic:
|
|
145
151
|
- If directory doesn't exist: extract from ZIP
|
|
146
152
|
- If marker file missing: extract from ZIP
|
|
@@ -154,12 +160,18 @@ class NextJSStaticView(ZipExtractionMixin, View):
|
|
|
154
160
|
- /cfg/admin/private/ → /cfg/admin/private.html (fallback)
|
|
155
161
|
- /cfg/admin/tasks → /cfg/admin/tasks.html
|
|
156
162
|
- /cfg/admin/tasks → /cfg/admin/tasks/index.html (fallback)
|
|
163
|
+
|
|
164
|
+
Compression examples:
|
|
165
|
+
- _app.js (br supported) → _app.js.br + Content-Encoding: br
|
|
166
|
+
- _app.js (gzip supported) → _app.js.gz + Content-Encoding: gzip
|
|
167
|
+
- _app.js (no support) → _app.js (uncompressed)
|
|
168
|
+
- index.html → index.html (never compressed, needs JWT injection)
|
|
157
169
|
"""
|
|
158
170
|
|
|
159
171
|
app_name = 'admin'
|
|
160
172
|
|
|
161
173
|
def get(self, request, path=''):
|
|
162
|
-
"""Serve static files from Next.js build with JWT injection."""
|
|
174
|
+
"""Serve static files from Next.js build with JWT injection and compression support."""
|
|
163
175
|
import django_cfg
|
|
164
176
|
|
|
165
177
|
base_dir = Path(django_cfg.__file__).parent / 'static' / 'frontend' / self.app_name
|
|
@@ -191,8 +203,18 @@ class NextJSStaticView(ZipExtractionMixin, View):
|
|
|
191
203
|
request.META.pop('HTTP_IF_MODIFIED_SINCE', None)
|
|
192
204
|
request.META.pop('HTTP_IF_NONE_MATCH', None)
|
|
193
205
|
|
|
194
|
-
#
|
|
195
|
-
|
|
206
|
+
# Try to serve precompressed file if browser supports it
|
|
207
|
+
compressed_path, encoding = self._find_precompressed_file(base_dir, path, request)
|
|
208
|
+
if compressed_path:
|
|
209
|
+
logger.debug(f"[Compression] Serving {encoding} for {path}")
|
|
210
|
+
response = serve(request, compressed_path, document_root=str(base_dir))
|
|
211
|
+
response['Content-Encoding'] = encoding
|
|
212
|
+
# Remove Content-Length as it's incorrect for compressed content
|
|
213
|
+
if 'Content-Length' in response:
|
|
214
|
+
del response['Content-Length']
|
|
215
|
+
else:
|
|
216
|
+
# Serve the static file normally
|
|
217
|
+
response = serve(request, path, document_root=str(base_dir))
|
|
196
218
|
|
|
197
219
|
# Convert FileResponse to HttpResponse for HTML files to enable JWT injection
|
|
198
220
|
if isinstance(response, FileResponse):
|
|
@@ -222,6 +244,65 @@ class NextJSStaticView(ZipExtractionMixin, View):
|
|
|
222
244
|
|
|
223
245
|
return response
|
|
224
246
|
|
|
247
|
+
def _find_precompressed_file(self, base_dir, path, request):
|
|
248
|
+
"""
|
|
249
|
+
Find and return precompressed file (.br or .gz) if available and supported by browser.
|
|
250
|
+
|
|
251
|
+
Brotli (.br) is preferred over Gzip (.gz) as it provides better compression.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
base_dir: Base directory for static files
|
|
255
|
+
path: Requested file path
|
|
256
|
+
request: Django request object
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
tuple: (compressed_path, encoding) if precompressed file found and supported,
|
|
260
|
+
(None, None) otherwise
|
|
261
|
+
|
|
262
|
+
Examples:
|
|
263
|
+
_app.js → _app.js.br (if Accept-Encoding: br)
|
|
264
|
+
_app.js → _app.js.gz (if Accept-Encoding: gzip, no .br)
|
|
265
|
+
_app.js → (None, None) (if no precompressed files or not supported)
|
|
266
|
+
"""
|
|
267
|
+
# Get Accept-Encoding header
|
|
268
|
+
accept_encoding = request.META.get('HTTP_ACCEPT_ENCODING', '').lower()
|
|
269
|
+
|
|
270
|
+
# Check if browser supports brotli (preferred) or gzip
|
|
271
|
+
supports_br = 'br' in accept_encoding
|
|
272
|
+
supports_gzip = 'gzip' in accept_encoding
|
|
273
|
+
|
|
274
|
+
if not (supports_br or supports_gzip):
|
|
275
|
+
return None, None
|
|
276
|
+
|
|
277
|
+
# Don't compress HTML files - we need to inject JWT tokens
|
|
278
|
+
# JWT injection requires modifying content, which is incompatible with compression
|
|
279
|
+
if path.endswith('.html'):
|
|
280
|
+
return None, None
|
|
281
|
+
|
|
282
|
+
# Build full file path
|
|
283
|
+
file_path = base_dir / path
|
|
284
|
+
|
|
285
|
+
# Check if original file exists (safety check)
|
|
286
|
+
if not file_path.exists() or not file_path.is_file():
|
|
287
|
+
return None, None
|
|
288
|
+
|
|
289
|
+
# Try Brotli first (better compression, ~5-15% smaller than gzip)
|
|
290
|
+
if supports_br:
|
|
291
|
+
br_path = f"{path}.br"
|
|
292
|
+
br_file = base_dir / br_path
|
|
293
|
+
if br_file.exists() and br_file.is_file():
|
|
294
|
+
return br_path, 'br'
|
|
295
|
+
|
|
296
|
+
# Fallback to Gzip
|
|
297
|
+
if supports_gzip:
|
|
298
|
+
gz_path = f"{path}.gz"
|
|
299
|
+
gz_file = base_dir / gz_path
|
|
300
|
+
if gz_file.exists() and gz_file.is_file():
|
|
301
|
+
return gz_path, 'gzip'
|
|
302
|
+
|
|
303
|
+
# No precompressed file found or not supported
|
|
304
|
+
return None, None
|
|
305
|
+
|
|
225
306
|
def _resolve_spa_path(self, base_dir, path):
|
|
226
307
|
"""
|
|
227
308
|
Resolve SPA path with multiple fallback strategies.
|
|
@@ -7,16 +7,19 @@ Usage: python manage.py maintenance enable/disable/status/sync domain.com
|
|
|
7
7
|
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from django.core.management.base import
|
|
10
|
+
from django.core.management.base import CommandError
|
|
11
11
|
from django.db import transaction
|
|
12
12
|
|
|
13
|
+
from django_cfg.management.utils import InteractiveCommand
|
|
14
|
+
|
|
13
15
|
from ...models import CloudflareSite, MaintenanceLog
|
|
14
16
|
from ...services import MaintenanceService
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
class Command(
|
|
19
|
+
class Command(InteractiveCommand):
|
|
18
20
|
"""Simple maintenance management command."""
|
|
19
21
|
|
|
22
|
+
command_name = 'maintenance'
|
|
20
23
|
help = 'Manage maintenance mode for Cloudflare sites'
|
|
21
24
|
|
|
22
25
|
def add_arguments(self, parser) -> None:
|
|
@@ -5,16 +5,18 @@ Handles automatic start/stop of scheduled maintenance windows.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
from django.core.management.base import BaseCommand
|
|
9
8
|
from django.utils import timezone
|
|
10
9
|
|
|
10
|
+
from django_cfg.management.utils import AdminCommand
|
|
11
|
+
|
|
11
12
|
from ...models import ScheduledMaintenance
|
|
12
13
|
from ...services.scheduled_maintenance_service import scheduled_maintenance_service
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
class Command(
|
|
16
|
+
class Command(AdminCommand):
|
|
16
17
|
"""Process scheduled maintenance events."""
|
|
17
18
|
|
|
19
|
+
command_name = 'process_scheduled_maintenance'
|
|
18
20
|
help = 'Process scheduled maintenance events (start due, complete overdue)'
|
|
19
21
|
|
|
20
22
|
def add_arguments(self, parser):
|