django-cfg 1.4.111__py3-none-any.whl → 1.4.114__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.

Files changed (35) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/dashboard/serializers/__init__.py +12 -0
  3. django_cfg/apps/dashboard/serializers/django_q2.py +50 -0
  4. django_cfg/apps/dashboard/serializers/overview.py +22 -11
  5. django_cfg/apps/dashboard/services/__init__.py +2 -0
  6. django_cfg/apps/dashboard/services/django_q2_service.py +159 -0
  7. django_cfg/apps/dashboard/services/system_health_service.py +85 -0
  8. django_cfg/apps/dashboard/urls.py +2 -0
  9. django_cfg/apps/dashboard/views/__init__.py +2 -0
  10. django_cfg/apps/dashboard/views/django_q2_views.py +79 -0
  11. django_cfg/apps/dashboard/views/overview_views.py +16 -2
  12. django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +16 -0
  13. django_cfg/config.py +3 -4
  14. django_cfg/core/base/config_model.py +18 -1
  15. django_cfg/core/builders/apps_builder.py +4 -0
  16. django_cfg/core/generation/data_generators/cache.py +28 -2
  17. django_cfg/core/generation/integration_generators/__init__.py +4 -0
  18. django_cfg/core/generation/integration_generators/django_q2.py +133 -0
  19. django_cfg/core/generation/orchestrator.py +13 -0
  20. django_cfg/core/integration/display/startup.py +2 -2
  21. django_cfg/core/integration/url_integration.py +2 -2
  22. django_cfg/models/__init__.py +3 -0
  23. django_cfg/models/django/__init__.py +3 -0
  24. django_cfg/models/django/django_q2.py +491 -0
  25. django_cfg/modules/django_admin/utils/html_builder.py +50 -2
  26. django_cfg/pyproject.toml +2 -2
  27. django_cfg/registry/core.py +4 -0
  28. django_cfg/static/frontend/admin.zip +0 -0
  29. django_cfg/templates/admin/index.html +389 -166
  30. django_cfg/templatetags/django_cfg.py +8 -0
  31. {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/METADATA +3 -1
  32. {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/RECORD +35 -29
  33. {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/WHEEL +0 -0
  34. {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/entry_points.txt +0 -0
  35. {django_cfg-1.4.111.dist-info → django_cfg-1.4.114.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.111"
35
+ __version__ = "1.4.114"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -22,6 +22,12 @@ from .commands import (
22
22
  CommandHelpResponseSerializer,
23
23
  )
24
24
  from .apizones import APIZoneSerializer, APIZonesSummarySerializer
25
+ from .django_q2 import (
26
+ DjangoQ2ScheduleSerializer,
27
+ DjangoQ2TaskSerializer,
28
+ DjangoQ2StatusSerializer,
29
+ DjangoQ2SummarySerializer,
30
+ )
25
31
 
26
32
  __all__ = [
27
33
  # Base
@@ -59,4 +65,10 @@ __all__ = [
59
65
  # API Zones
60
66
  'APIZoneSerializer',
61
67
  'APIZonesSummarySerializer',
68
+
69
+ # Django-Q2
70
+ 'DjangoQ2ScheduleSerializer',
71
+ 'DjangoQ2TaskSerializer',
72
+ 'DjangoQ2StatusSerializer',
73
+ 'DjangoQ2SummarySerializer',
62
74
  ]
@@ -0,0 +1,50 @@
1
+ """
2
+ Django-Q2 Task Serializers
3
+
4
+ Serializers for displaying Django-Q2 scheduled tasks and task history.
5
+ """
6
+
7
+ from rest_framework import serializers
8
+
9
+
10
+ class DjangoQ2ScheduleSerializer(serializers.Serializer):
11
+ """Serializer for Django-Q2 scheduled tasks."""
12
+
13
+ id = serializers.IntegerField(read_only=True)
14
+ name = serializers.CharField(max_length=255)
15
+ func = serializers.CharField(max_length=512)
16
+ schedule_type = serializers.CharField(max_length=24)
17
+ repeats = serializers.IntegerField()
18
+ next_run = serializers.DateTimeField(allow_null=True)
19
+ last_run = serializers.DateTimeField(allow_null=True)
20
+
21
+
22
+ class DjangoQ2TaskSerializer(serializers.Serializer):
23
+ """Serializer for Django-Q2 task execution history."""
24
+
25
+ id = serializers.CharField(max_length=32)
26
+ name = serializers.CharField(max_length=255)
27
+ func = serializers.CharField(max_length=512)
28
+ started = serializers.DateTimeField()
29
+ stopped = serializers.DateTimeField(allow_null=True)
30
+ success = serializers.BooleanField()
31
+ result = serializers.CharField(allow_null=True)
32
+
33
+
34
+ class DjangoQ2StatusSerializer(serializers.Serializer):
35
+ """Serializer for Django-Q2 cluster status."""
36
+
37
+ cluster_running = serializers.BooleanField()
38
+ total_schedules = serializers.IntegerField()
39
+ active_schedules = serializers.IntegerField()
40
+ recent_tasks = serializers.IntegerField()
41
+ successful_tasks = serializers.IntegerField()
42
+ failed_tasks = serializers.IntegerField()
43
+
44
+
45
+ class DjangoQ2SummarySerializer(serializers.Serializer):
46
+ """Summary serializer for Django-Q2 dashboard."""
47
+
48
+ status = DjangoQ2StatusSerializer()
49
+ schedules = DjangoQ2ScheduleSerializer(many=True)
50
+ recent_tasks = DjangoQ2TaskSerializer(many=True)
@@ -5,30 +5,41 @@ Serializers for dashboard overview endpoint.
5
5
  """
6
6
 
7
7
  from rest_framework import serializers
8
+ from .statistics import StatCardSerializer, UserStatisticsSerializer, AppStatisticsSerializer
9
+ from .system import SystemHealthSerializer, SystemMetricsSerializer
10
+ from .activity import QuickActionSerializer, ActivityEntrySerializer
8
11
 
9
12
 
10
13
  class DashboardOverviewSerializer(serializers.Serializer):
11
14
  """
12
15
  Main serializer for dashboard overview endpoint.
13
- Uses DictField to avoid allOf generation in OpenAPI.
16
+ Uses typed serializers for proper OpenAPI schema generation.
14
17
  """
15
18
 
16
- stat_cards = serializers.ListField(
17
- child=serializers.DictField(),
19
+ stat_cards = StatCardSerializer(
20
+ many=True,
18
21
  help_text="Dashboard statistics cards"
19
22
  )
20
- system_health = serializers.ListField(
21
- child=serializers.DictField(),
23
+ system_health = SystemHealthSerializer(
22
24
  help_text="System health status"
23
25
  )
24
- quick_actions = serializers.ListField(
25
- child=serializers.DictField(),
26
+ quick_actions = QuickActionSerializer(
27
+ many=True,
26
28
  help_text="Quick action buttons"
27
29
  )
28
- recent_activity = serializers.ListField(
29
- child=serializers.DictField(),
30
+ recent_activity = ActivityEntrySerializer(
31
+ many=True,
30
32
  help_text="Recent activity entries"
31
33
  )
32
- system_metrics = serializers.DictField(help_text="System performance metrics")
33
- user_statistics = serializers.DictField(help_text="User statistics")
34
+ system_metrics = SystemMetricsSerializer(
35
+ help_text="System performance metrics"
36
+ )
37
+ user_statistics = UserStatisticsSerializer(
38
+ help_text="User statistics"
39
+ )
40
+ app_statistics = AppStatisticsSerializer(
41
+ many=True,
42
+ required=False,
43
+ help_text="Application statistics"
44
+ )
34
45
  timestamp = serializers.CharField(help_text="Data timestamp (ISO format)")
@@ -11,6 +11,7 @@ from .charts_service import ChartsService
11
11
  from .commands_service import CommandsService
12
12
  from .apizones_service import APIZonesService
13
13
  from .overview_service import OverviewService
14
+ from .django_q2_service import DjangoQ2Service
14
15
 
15
16
  __all__ = [
16
17
  'StatisticsService',
@@ -19,4 +20,5 @@ __all__ = [
19
20
  'CommandsService',
20
21
  'APIZonesService',
21
22
  'OverviewService',
23
+ 'DjangoQ2Service',
22
24
  ]
@@ -0,0 +1,159 @@
1
+ """
2
+ Django-Q2 Service
3
+
4
+ Business logic for collecting Django-Q2 task data.
5
+ """
6
+
7
+ from typing import Dict, List, Any
8
+
9
+
10
+ class DjangoQ2Service:
11
+ """Service for aggregating Django-Q2 task information."""
12
+
13
+ @staticmethod
14
+ def get_schedules() -> List[Dict[str, Any]]:
15
+ """
16
+ Get all scheduled tasks from Django-Q2.
17
+
18
+ Returns:
19
+ List of scheduled tasks with their configurations
20
+ """
21
+ try:
22
+ from django_q.models import Schedule
23
+
24
+ schedules = Schedule.objects.all().order_by('-next_run')
25
+ return [
26
+ {
27
+ 'id': schedule.id,
28
+ 'name': schedule.name,
29
+ 'func': schedule.func,
30
+ 'schedule_type': schedule.schedule_type,
31
+ 'repeats': schedule.repeats,
32
+ 'next_run': schedule.next_run,
33
+ 'last_run': getattr(schedule, 'last_run', None),
34
+ }
35
+ for schedule in schedules
36
+ ]
37
+ except ImportError:
38
+ # django-q2 not installed
39
+ return []
40
+ except Exception:
41
+ # Database error or table doesn't exist yet
42
+ return []
43
+
44
+ @staticmethod
45
+ def get_recent_tasks(limit: int = 20) -> List[Dict[str, Any]]:
46
+ """
47
+ Get recent task executions from Django-Q2.
48
+
49
+ Args:
50
+ limit: Maximum number of tasks to return
51
+
52
+ Returns:
53
+ List of recent task executions with their results
54
+ """
55
+ try:
56
+ from django_q.models import Task
57
+
58
+ tasks = Task.objects.all().order_by('-started')[:limit]
59
+ return [
60
+ {
61
+ 'id': task.id,
62
+ 'name': task.name,
63
+ 'func': task.func,
64
+ 'started': task.started,
65
+ 'stopped': task.stopped,
66
+ 'success': task.success,
67
+ 'result': str(task.result) if task.result else None,
68
+ }
69
+ for task in tasks
70
+ ]
71
+ except ImportError:
72
+ # django-q2 not installed
73
+ return []
74
+ except Exception:
75
+ # Database error or table doesn't exist yet
76
+ return []
77
+
78
+ @staticmethod
79
+ def get_cluster_status() -> Dict[str, Any]:
80
+ """
81
+ Get Django-Q2 cluster status.
82
+
83
+ Returns:
84
+ Dictionary with cluster status information
85
+ """
86
+ try:
87
+ from django_q.models import Schedule, Task
88
+ from django_q.cluster import Cluster
89
+
90
+ # Count schedules
91
+ total_schedules = Schedule.objects.count()
92
+ active_schedules = Schedule.objects.filter(repeats__gt=0).count()
93
+
94
+ # Count recent tasks (last 24 hours)
95
+ from django.utils import timezone
96
+ from datetime import timedelta
97
+
98
+ last_24h = timezone.now() - timedelta(hours=24)
99
+ recent_tasks = Task.objects.filter(started__gte=last_24h).count()
100
+ successful_tasks = Task.objects.filter(
101
+ started__gte=last_24h, success=True
102
+ ).count()
103
+ failed_tasks = Task.objects.filter(
104
+ started__gte=last_24h, success=False
105
+ ).count()
106
+
107
+ # Check if cluster is running
108
+ cluster_running = False
109
+ try:
110
+ # Check for recent task activity as proxy for cluster status
111
+ recent_task = Task.objects.filter(
112
+ started__gte=timezone.now() - timedelta(minutes=5)
113
+ ).exists()
114
+ cluster_running = recent_task
115
+ except Exception:
116
+ pass
117
+
118
+ return {
119
+ 'cluster_running': cluster_running,
120
+ 'total_schedules': total_schedules,
121
+ 'active_schedules': active_schedules,
122
+ 'recent_tasks': recent_tasks,
123
+ 'successful_tasks': successful_tasks,
124
+ 'failed_tasks': failed_tasks,
125
+ }
126
+ except ImportError:
127
+ # django-q2 not installed
128
+ return {
129
+ 'cluster_running': False,
130
+ 'total_schedules': 0,
131
+ 'active_schedules': 0,
132
+ 'recent_tasks': 0,
133
+ 'successful_tasks': 0,
134
+ 'failed_tasks': 0,
135
+ }
136
+ except Exception:
137
+ # Database error or table doesn't exist yet
138
+ return {
139
+ 'cluster_running': False,
140
+ 'total_schedules': 0,
141
+ 'active_schedules': 0,
142
+ 'recent_tasks': 0,
143
+ 'successful_tasks': 0,
144
+ 'failed_tasks': 0,
145
+ }
146
+
147
+ @classmethod
148
+ def get_summary(cls) -> Dict[str, Any]:
149
+ """
150
+ Get complete Django-Q2 summary for dashboard.
151
+
152
+ Returns:
153
+ Dictionary with status, schedules, and recent tasks
154
+ """
155
+ return {
156
+ 'status': cls.get_cluster_status(),
157
+ 'schedules': cls.get_schedules(),
158
+ 'recent_tasks': cls.get_recent_tasks(limit=10),
159
+ }
@@ -191,6 +191,90 @@ class SystemHealthService:
191
191
  'health_percentage': 0,
192
192
  }
193
193
 
194
+ def check_django_q2_health(self) -> Dict[str, Any]:
195
+ """
196
+ Check Django-Q2 task scheduling configuration and status.
197
+
198
+ Returns:
199
+ Health status dictionary with schedule count and cluster status
200
+ """
201
+ try:
202
+ from django_cfg.core.config import get_current_config
203
+
204
+ config = get_current_config()
205
+
206
+ # Check if django_q2 is configured
207
+ if not hasattr(config, 'django_q2') or not config.django_q2:
208
+ return {
209
+ 'component': 'django_q2',
210
+ 'status': 'info',
211
+ 'description': 'Django-Q2 scheduling not configured',
212
+ 'last_check': datetime.now().isoformat(),
213
+ 'health_percentage': 100,
214
+ 'details': {
215
+ 'enabled': False,
216
+ 'schedules_count': 0,
217
+ }
218
+ }
219
+
220
+ django_q2_config = config.django_q2
221
+
222
+ # Check if enabled
223
+ if not django_q2_config.enabled:
224
+ return {
225
+ 'component': 'django_q2',
226
+ 'status': 'warning',
227
+ 'description': 'Django-Q2 scheduling is disabled',
228
+ 'last_check': datetime.now().isoformat(),
229
+ 'health_percentage': 50,
230
+ 'details': {
231
+ 'enabled': False,
232
+ 'schedules_count': len(django_q2_config.schedules) if django_q2_config.schedules else 0,
233
+ }
234
+ }
235
+
236
+ # Count schedules
237
+ schedules_count = len(django_q2_config.schedules) if django_q2_config.schedules else 0
238
+
239
+ # Try to check cluster status from database
240
+ cluster_running = False
241
+ try:
242
+ from django_q.models import Schedule, Task
243
+ from django.utils import timezone
244
+ from datetime import timedelta
245
+
246
+ # Check for recent task activity
247
+ recent_task = Task.objects.filter(
248
+ started__gte=timezone.now() - timedelta(minutes=5)
249
+ ).exists()
250
+ cluster_running = recent_task
251
+ except Exception:
252
+ pass
253
+
254
+ return {
255
+ 'component': 'django_q2',
256
+ 'status': 'healthy',
257
+ 'description': f'{schedules_count} schedule(s) configured, cluster {"running" if cluster_running else "idle"}',
258
+ 'last_check': datetime.now().isoformat(),
259
+ 'health_percentage': 100,
260
+ 'details': {
261
+ 'enabled': True,
262
+ 'schedules_count': schedules_count,
263
+ 'cluster_running': cluster_running,
264
+ 'workers': django_q2_config.workers,
265
+ }
266
+ }
267
+
268
+ except Exception as e:
269
+ self.logger.error(f"Django-Q2 health check failed: {e}")
270
+ return {
271
+ 'component': 'django_q2',
272
+ 'status': 'error',
273
+ 'description': f'Django-Q2 check error: {str(e)}',
274
+ 'last_check': datetime.now().isoformat(),
275
+ 'health_percentage': 0,
276
+ }
277
+
194
278
  def get_all_health_checks(self) -> List[Dict[str, Any]]:
195
279
  """
196
280
  Run all health checks and return aggregated results.
@@ -205,6 +289,7 @@ class SystemHealthService:
205
289
  self.check_cache_health(),
206
290
  self.check_queue_health(),
207
291
  self.check_storage_health(),
292
+ self.check_django_q2_health(),
208
293
  ]
209
294
 
210
295
  return checks
@@ -21,6 +21,7 @@ from .views import (
21
21
  ChartsViewSet,
22
22
  CommandsViewSet,
23
23
  APIZonesViewSet,
24
+ DjangoQ2ViewSet,
24
25
  )
25
26
 
26
27
  app_name = 'django_cfg_dashboard'
@@ -34,6 +35,7 @@ router.register(r'activity', ActivityViewSet, basename='activity')
34
35
  router.register(r'charts', ChartsViewSet, basename='charts')
35
36
  router.register(r'commands', CommandsViewSet, basename='commands')
36
37
  router.register(r'zones', APIZonesViewSet, basename='zones')
38
+ router.register(r'django_q2', DjangoQ2ViewSet, basename='django_q2')
37
39
 
38
40
  urlpatterns = [
39
41
  # RESTful API endpoints using ViewSets
@@ -11,6 +11,7 @@ from .activity_views import ActivityViewSet
11
11
  from .charts_views import ChartsViewSet
12
12
  from .commands_views import CommandsViewSet
13
13
  from .apizones_views import APIZonesViewSet
14
+ from .django_q2_views import DjangoQ2ViewSet
14
15
 
15
16
  __all__ = [
16
17
  'OverviewViewSet',
@@ -20,4 +21,5 @@ __all__ = [
20
21
  'ChartsViewSet',
21
22
  'CommandsViewSet',
22
23
  'APIZonesViewSet',
24
+ 'DjangoQ2ViewSet',
23
25
  ]
@@ -0,0 +1,79 @@
1
+ """
2
+ Django-Q2 ViewSet
3
+
4
+ API endpoints for Django-Q2 task monitoring.
5
+ """
6
+
7
+ from rest_framework import viewsets, status
8
+ from rest_framework.decorators import action
9
+ from rest_framework.response import Response
10
+ from rest_framework.permissions import IsAuthenticated
11
+
12
+ from ..services.django_q2_service import DjangoQ2Service
13
+ from ..serializers.django_q2 import (
14
+ DjangoQ2ScheduleSerializer,
15
+ DjangoQ2TaskSerializer,
16
+ DjangoQ2StatusSerializer,
17
+ DjangoQ2SummarySerializer,
18
+ )
19
+
20
+
21
+ class DjangoQ2ViewSet(viewsets.ViewSet):
22
+ """
23
+ ViewSet for Django-Q2 task monitoring.
24
+
25
+ Provides endpoints for:
26
+ - Scheduled tasks list
27
+ - Recent task executions
28
+ - Cluster status
29
+ - Complete summary
30
+ """
31
+
32
+ permission_classes = [IsAuthenticated]
33
+
34
+ @action(detail=False, methods=['get'])
35
+ def schedules(self, request):
36
+ """
37
+ Get all scheduled tasks.
38
+
39
+ GET /api/django_q2/schedules/
40
+ """
41
+ schedules = DjangoQ2Service.get_schedules()
42
+ serializer = DjangoQ2ScheduleSerializer(schedules, many=True)
43
+ return Response(serializer.data)
44
+
45
+ @action(detail=False, methods=['get'])
46
+ def tasks(self, request):
47
+ """
48
+ Get recent task executions.
49
+
50
+ GET /api/django_q2/tasks/
51
+ Query params:
52
+ - limit: Number of tasks to return (default: 20)
53
+ """
54
+ limit = int(request.query_params.get('limit', 20))
55
+ tasks = DjangoQ2Service.get_recent_tasks(limit=limit)
56
+ serializer = DjangoQ2TaskSerializer(tasks, many=True)
57
+ return Response(serializer.data)
58
+
59
+ @action(detail=False, methods=['get'])
60
+ def status(self, request):
61
+ """
62
+ Get Django-Q2 cluster status.
63
+
64
+ GET /api/django_q2/status/
65
+ """
66
+ status_data = DjangoQ2Service.get_cluster_status()
67
+ serializer = DjangoQ2StatusSerializer(status_data)
68
+ return Response(serializer.data)
69
+
70
+ def list(self, request):
71
+ """
72
+ Get complete Django-Q2 summary.
73
+
74
+ GET /api/django_q2/
75
+ Returns status, schedules, and recent tasks.
76
+ """
77
+ summary = DjangoQ2Service.get_summary()
78
+ serializer = DjangoQ2SummarySerializer(summary)
79
+ return Response(serializer.data)
@@ -56,14 +56,28 @@ class OverviewViewSet(AdminAPIMixin, viewsets.GenericViewSet):
56
56
  health_service = SystemHealthService()
57
57
  charts_service = ChartsService()
58
58
 
59
+ # Get app statistics and convert to list format
60
+ app_stats_dict = stats_service.get_app_statistics()
61
+ app_statistics_list = [
62
+ {
63
+ 'app_name': app_label,
64
+ 'statistics': {
65
+ 'name': app_data.get('name', ''),
66
+ 'total_records': app_data.get('total_records', 0),
67
+ 'model_count': app_data.get('model_count', 0),
68
+ }
69
+ }
70
+ for app_label, app_data in app_stats_dict.get('apps', {}).items()
71
+ ]
72
+
59
73
  data = {
60
74
  # Statistics
61
75
  'stat_cards': stats_service.get_stat_cards(),
62
76
  'user_statistics': stats_service.get_user_statistics(),
63
- 'app_statistics': stats_service.get_app_statistics(),
77
+ 'app_statistics': app_statistics_list,
64
78
 
65
79
  # System
66
- 'system_health': health_service.get_all_health_checks(),
80
+ 'system_health': health_service.get_overall_health_status(),
67
81
  'system_metrics': stats_service.get_system_metrics(),
68
82
 
69
83
  # Activity
@@ -0,0 +1,16 @@
1
+ # Generated by Django 5.2.7 on 2025-10-31 05:54
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("tasks", "0001_initial"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.DeleteModel(
14
+ name="TaskLog",
15
+ ),
16
+ ]
django_cfg/config.py CHANGED
@@ -12,15 +12,14 @@ from .modules.django_unfold.models.dropdown import SiteDropdownItem
12
12
  # Library configuration
13
13
  LIB_NAME = "django-cfg"
14
14
  LIB_SITE_URL = "https://djangocfg.com"
15
- LIB_DOCS_URL = "https://docs.djangocfg.com"
16
15
  LIB_GITHUB_URL = "https://github.com/django-cfg/django-cfg"
17
- LIB_SUPPORT_URL = "https://djangocfg.com/support"
16
+ LIB_SUPPORT_URL = "https://demo.djangocfg.com"
18
17
  LIB_HEALTH_URL = "/cfg/health/"
19
18
 
20
19
  def get_maintenance_url(domain: str) -> str:
21
20
  """Get the maintenance URL for the current site."""
22
21
  # return f"{LIB_SITE_URL}/maintenance/{domain}/"
23
- return f"{LIB_DOCS_URL}/maintenance?site={domain}"
22
+ return f"{LIB_SITE_URL}/maintenance?site={domain}"
24
23
 
25
24
  def get_default_dropdown_items() -> List[SiteDropdownItem]:
26
25
  """Get default dropdown menu items for Unfold admin."""
@@ -28,7 +27,7 @@ def get_default_dropdown_items() -> List[SiteDropdownItem]:
28
27
  SiteDropdownItem(
29
28
  title="Documentation",
30
29
  icon=Icons.HELP_OUTLINE,
31
- link=LIB_DOCS_URL,
30
+ link=LIB_SITE_URL,
32
31
  ),
33
32
  SiteDropdownItem(
34
33
  title="GitHub",
@@ -20,6 +20,7 @@ from ...models import (
20
20
  ApiKeys,
21
21
  AxesConfig,
22
22
  CacheConfig,
23
+ DjangoQ2Config,
23
24
  CryptoFieldsConfig,
24
25
  DatabaseConfig,
25
26
  DRFConfig,
@@ -216,9 +217,19 @@ class DjangoConfig(BaseModel):
216
217
  )
217
218
 
218
219
  # === Cache Configuration ===
220
+ # Redis URL - used for automatic cache configuration if cache_default is not set
221
+ redis_url: Optional[str] = Field(
222
+ default=None,
223
+ description=(
224
+ "Redis connection URL (redis://host:port/db). "
225
+ "If set and cache_default is None, automatically creates RedisCache backend. "
226
+ "Also used by tasks (ReArq, Django-Q2) if they don't specify broker_url."
227
+ ),
228
+ )
229
+
219
230
  cache_default: Optional[CacheConfig] = Field(
220
231
  default=None,
221
- description="Default cache backend",
232
+ description="Default cache backend (auto-created from redis_url if not set)",
222
233
  )
223
234
 
224
235
  cache_sessions: Optional[CacheConfig] = Field(
@@ -318,6 +329,12 @@ class DjangoConfig(BaseModel):
318
329
  description="Background task processing configuration (ReArq)",
319
330
  )
320
331
 
332
+ # === Django-Q2 Task Scheduling ===
333
+ django_q2: Optional[DjangoQ2Config] = Field(
334
+ default=None,
335
+ description="Django-Q2 task scheduling and queue configuration",
336
+ )
337
+
321
338
  # === Centrifugo Configuration ===
322
339
  centrifugo: Optional[DjangoCfgCentrifugoConfig] = Field(
323
340
  default=None,
@@ -163,6 +163,10 @@ class InstalledAppsBuilder:
163
163
  # No external app needed - ReArq is embedded
164
164
  apps.append("django_cfg.apps.tasks")
165
165
 
166
+ # Add django-q2 if enabled
167
+ if hasattr(self.config, "django_q2") and self.config.django_q2 and self.config.django_q2.enabled:
168
+ apps.append("django_q")
169
+
166
170
  # Add DRF Tailwind theme module (uses Tailwind via CDN)
167
171
  if self.config.enable_drf_tailwind:
168
172
  apps.append("django_cfg.modules.django_drf_theme.apps.DjangoDRFThemeConfig")