django-cfg 1.4.107__py3-none-any.whl → 1.4.109__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/accounts/views/profile.py +19 -9
- django_cfg/apps/centrifugo/views/admin_api.py +4 -7
- django_cfg/apps/centrifugo/views/monitoring.py +3 -6
- django_cfg/apps/centrifugo/views/testing_api.py +3 -6
- django_cfg/apps/dashboard/services/system_health_service.py +16 -11
- django_cfg/apps/dashboard/views/activity_views.py +3 -5
- django_cfg/apps/dashboard/views/apizones_views.py +4 -5
- django_cfg/apps/dashboard/views/charts_views.py +4 -5
- django_cfg/apps/dashboard/views/overview_views.py +4 -5
- django_cfg/apps/dashboard/views/statistics_views.py +4 -5
- django_cfg/apps/dashboard/views/system_views.py +4 -5
- django_cfg/apps/knowbase/__init__.py +2 -2
- django_cfg/apps/knowbase/apps.py +2 -8
- django_cfg/apps/knowbase/views/base.py +9 -4
- django_cfg/apps/support/views/api.py +16 -7
- django_cfg/apps/tasks/__init__.py +61 -2
- django_cfg/apps/tasks/admin/__init__.py +3 -10
- django_cfg/apps/tasks/admin/config.py +98 -0
- django_cfg/apps/tasks/admin/task_log.py +265 -0
- django_cfg/apps/tasks/apps.py +7 -9
- django_cfg/apps/tasks/filters/__init__.py +10 -0
- django_cfg/apps/tasks/filters/task_log.py +121 -0
- django_cfg/apps/tasks/migrations/0001_initial.py +196 -0
- django_cfg/apps/tasks/models/__init__.py +4 -0
- django_cfg/apps/tasks/models/task_log.py +246 -0
- django_cfg/apps/tasks/serializers/__init__.py +28 -0
- django_cfg/apps/tasks/serializers/task_log.py +249 -0
- django_cfg/apps/tasks/services/__init__.py +10 -0
- django_cfg/apps/tasks/services/client/__init__.py +7 -0
- django_cfg/apps/tasks/services/client/client.py +234 -0
- django_cfg/apps/tasks/services/config_helper.py +63 -0
- django_cfg/apps/tasks/services/sync.py +204 -0
- django_cfg/apps/tasks/urls.py +7 -13
- django_cfg/apps/tasks/views/__init__.py +4 -10
- django_cfg/apps/tasks/views/task_log.py +41 -0
- django_cfg/apps/tasks/views/task_log_base.py +41 -0
- django_cfg/apps/tasks/views/task_log_overview.py +100 -0
- django_cfg/apps/tasks/views/task_log_related.py +41 -0
- django_cfg/apps/tasks/views/task_log_stats.py +91 -0
- django_cfg/apps/tasks/views/task_log_timeline.py +81 -0
- django_cfg/apps/urls.py +0 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/utils.py +1 -1
- django_cfg/core/base/config_model.py +1 -1
- django_cfg/core/builders/apps_builder.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/tasks.py +14 -18
- django_cfg/core/generation/security_generators/crypto_fields.py +2 -1
- django_cfg/core/integration/display/startup.py +1 -1
- django_cfg/mixins/__init__.py +12 -0
- django_cfg/mixins/admin_api.py +37 -0
- django_cfg/mixins/client_api.py +39 -0
- django_cfg/models/django/constance.py +2 -8
- django_cfg/models/django/crypto_fields.py +13 -48
- django_cfg/models/tasks/__init__.py +8 -10
- django_cfg/models/tasks/backends.py +76 -207
- django_cfg/models/tasks/config.py +20 -127
- django_cfg/models/tasks/utils.py +17 -29
- django_cfg/modules/django_client/management/commands/generate_client.py +13 -1
- django_cfg/modules/django_unfold/navigation.py +121 -22
- django_cfg/pyproject.toml +2 -2
- django_cfg/registry/core.py +1 -1
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/METADATA +3 -3
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/RECORD +70 -117
- django_cfg/apps/tasks/admin/actions.py +0 -29
- django_cfg/apps/tasks/admin/tasks_admin.py +0 -154
- django_cfg/apps/tasks/api/serializers.py +0 -82
- django_cfg/apps/tasks/api/views.py +0 -571
- django_cfg/apps/tasks/serializers.py +0 -82
- django_cfg/apps/tasks/static/tasks/css/dashboard-alpine.css +0 -299
- django_cfg/apps/tasks/static/tasks/css/dashboard.css +0 -120
- django_cfg/apps/tasks/static/tasks/js/alpine/README.md +0 -47
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/index.js +0 -8
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/management.js +0 -123
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/pagination.js +0 -21
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/tasks.js +0 -101
- django_cfg/apps/tasks/static/tasks/js/alpine/actions/workers.js +0 -59
- django_cfg/apps/tasks/static/tasks/js/alpine/computed.js +0 -35
- django_cfg/apps/tasks/static/tasks/js/alpine/index.js +0 -148
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/index.js +0 -36
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/overview.js +0 -37
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/queues.js +0 -27
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/tasks.js +0 -32
- django_cfg/apps/tasks/static/tasks/js/alpine/loaders/workers.js +0 -21
- django_cfg/apps/tasks/static/tasks/js/alpine/state.js +0 -36
- django_cfg/apps/tasks/static/tasks/js/alpine/utils/formatters.js +0 -42
- django_cfg/apps/tasks/static/tasks/js/alpine/utils/helpers.js +0 -68
- django_cfg/apps/tasks/static/tasks/js/dashboard-alpine.js +0 -725
- django_cfg/apps/tasks/tasks/__init__.py +0 -10
- django_cfg/apps/tasks/tasks/demo_tasks.py +0 -127
- django_cfg/apps/tasks/templates/tasks/components/management_actions.html +0 -71
- django_cfg/apps/tasks/templates/tasks/components/overview_content.html +0 -94
- django_cfg/apps/tasks/templates/tasks/components/queues_content.html +0 -44
- django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +0 -45
- django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -151
- django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +0 -61
- django_cfg/apps/tasks/templates/tasks/components/tasks_mjs_integration.html +0 -269
- django_cfg/apps/tasks/templates/tasks/components/workers_content.html +0 -60
- django_cfg/apps/tasks/templates/tasks/layout/base.html +0 -20
- django_cfg/apps/tasks/templates/tasks/pages/dashboard-improved.html +0 -168
- django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +0 -77
- django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +0 -40
- django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +0 -40
- django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +0 -86
- django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +0 -90
- django_cfg/apps/tasks/urls_admin.py +0 -15
- django_cfg/apps/tasks/utils/__init__.py +0 -1
- django_cfg/apps/tasks/utils/simulator.py +0 -353
- django_cfg/apps/tasks/views/api.py +0 -571
- django_cfg/apps/tasks/views/dashboard.py +0 -89
- django_cfg/management/commands/rundramatiq.py +0 -24
- django_cfg/management/commands/rundramatiq_simulator.py +0 -22
- django_cfg/management/commands/task_clear.py +0 -25
- django_cfg/management/commands/task_status.py +0 -24
- django_cfg/modules/django_client/system/__init__.py +0 -24
- django_cfg/modules/django_client/system/base_generator.py +0 -123
- django_cfg/modules/django_client/system/generate_mjs_clients.py +0 -176
- django_cfg/modules/django_client/system/mjs_generator.py +0 -219
- django_cfg/modules/django_client/system/schema_parser.py +0 -199
- django_cfg/modules/django_client/system/templates/api_client.js.j2 +0 -87
- django_cfg/modules/django_client/system/templates/app_index.js.j2 +0 -13
- django_cfg/modules/django_client/system/templates/base_client.js.j2 +0 -166
- django_cfg/modules/django_client/system/templates/main_index.js.j2 +0 -80
- django_cfg/modules/django_client/system/templates/types.js.j2 +0 -24
- django_cfg/modules/django_tasks/__init__.py +0 -29
- django_cfg/modules/django_tasks/dramatiq_setup.py +0 -20
- django_cfg/modules/django_tasks/factory.py +0 -127
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +0 -253
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +0 -436
- django_cfg/modules/django_tasks/management/commands/task_clear.py +0 -226
- django_cfg/modules/django_tasks/management/commands/task_status.py +0 -257
- django_cfg/modules/django_tasks/service.py +0 -281
- django_cfg/modules/django_tasks/settings.py +0 -107
- /django_cfg/{modules/django_tasks/management → apps/tasks/migrations}/__init__.py +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.107.dist-info → django_cfg-1.4.109.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Admin configuration for Task models.
|
|
3
|
+
|
|
4
|
+
Declarative AdminConfig using PydanticAdmin patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django_cfg.modules.django_admin import (
|
|
8
|
+
AdminConfig,
|
|
9
|
+
BadgeField,
|
|
10
|
+
DateTimeField,
|
|
11
|
+
Icons,
|
|
12
|
+
UserField,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ..models import TaskLog
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Declarative configuration for TaskLog
|
|
19
|
+
tasklog_config = AdminConfig(
|
|
20
|
+
model=TaskLog,
|
|
21
|
+
|
|
22
|
+
# Performance optimization
|
|
23
|
+
select_related=["user"],
|
|
24
|
+
|
|
25
|
+
# List display
|
|
26
|
+
list_display=[
|
|
27
|
+
"task_name",
|
|
28
|
+
"queue_badge",
|
|
29
|
+
"status_badge",
|
|
30
|
+
"user",
|
|
31
|
+
"duration_display",
|
|
32
|
+
"retry_count",
|
|
33
|
+
"created_at",
|
|
34
|
+
"completed_at",
|
|
35
|
+
],
|
|
36
|
+
|
|
37
|
+
# Auto-generated display methods
|
|
38
|
+
display_fields=[
|
|
39
|
+
BadgeField(
|
|
40
|
+
name="queue_name",
|
|
41
|
+
title="Queue",
|
|
42
|
+
variant="info",
|
|
43
|
+
icon=Icons.LAYERS,
|
|
44
|
+
),
|
|
45
|
+
BadgeField(
|
|
46
|
+
name="status",
|
|
47
|
+
title="Status",
|
|
48
|
+
label_map={
|
|
49
|
+
"queued": "secondary",
|
|
50
|
+
"in_progress": "info",
|
|
51
|
+
"completed": "success",
|
|
52
|
+
"failed": "danger",
|
|
53
|
+
"canceled": "warning",
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
UserField(name="user", title="User", header=True),
|
|
57
|
+
DateTimeField(name="created_at", title="Created", ordering="created_at"),
|
|
58
|
+
DateTimeField(name="started_at", title="Started", ordering="started_at"),
|
|
59
|
+
DateTimeField(name="completed_at", title="Completed", ordering="completed_at"),
|
|
60
|
+
],
|
|
61
|
+
|
|
62
|
+
# Filters
|
|
63
|
+
list_filter=["status", "task_name", "queue_name", "created_at"],
|
|
64
|
+
search_fields=[
|
|
65
|
+
"job_id",
|
|
66
|
+
"task_name",
|
|
67
|
+
"worker_id",
|
|
68
|
+
"error_message",
|
|
69
|
+
"user__username",
|
|
70
|
+
"user__email",
|
|
71
|
+
],
|
|
72
|
+
|
|
73
|
+
# Autocomplete for user field
|
|
74
|
+
autocomplete_fields=["user"],
|
|
75
|
+
|
|
76
|
+
# Readonly fields
|
|
77
|
+
readonly_fields=[
|
|
78
|
+
"id",
|
|
79
|
+
"job_id",
|
|
80
|
+
"created_at",
|
|
81
|
+
"started_at",
|
|
82
|
+
"completed_at",
|
|
83
|
+
"duration_ms",
|
|
84
|
+
"worker_id",
|
|
85
|
+
],
|
|
86
|
+
|
|
87
|
+
# Date hierarchy
|
|
88
|
+
date_hierarchy="created_at",
|
|
89
|
+
|
|
90
|
+
# Per page
|
|
91
|
+
list_per_page=50,
|
|
92
|
+
|
|
93
|
+
# Ordering
|
|
94
|
+
ordering=["-created_at"],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["tasklog_config"]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task Log Admin.
|
|
3
|
+
|
|
4
|
+
PydanticAdmin for TaskLog model with custom computed fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from django.contrib import admin
|
|
10
|
+
from django_cfg.modules.django_admin import Icons, computed_field
|
|
11
|
+
from django_cfg.modules.django_admin.base import PydanticAdmin
|
|
12
|
+
|
|
13
|
+
from ..models import TaskLog
|
|
14
|
+
from .config import tasklog_config
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@admin.register(TaskLog)
|
|
18
|
+
class TaskLogAdmin(PydanticAdmin):
|
|
19
|
+
"""
|
|
20
|
+
Task log admin with analytics and filtering.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Color-coded status badges
|
|
24
|
+
- Duration display with performance indicators
|
|
25
|
+
- Retry count tracking
|
|
26
|
+
- Formatted JSON for arguments
|
|
27
|
+
- Error details with highlighted display
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
config = tasklog_config
|
|
31
|
+
|
|
32
|
+
@computed_field("Queue", ordering="queue_name")
|
|
33
|
+
def queue_badge(self, obj):
|
|
34
|
+
"""Display queue name as badge."""
|
|
35
|
+
variant_map = {
|
|
36
|
+
"critical": "danger",
|
|
37
|
+
"high": "warning",
|
|
38
|
+
"default": "primary",
|
|
39
|
+
"low": "secondary",
|
|
40
|
+
"background": "secondary",
|
|
41
|
+
}
|
|
42
|
+
variant = variant_map.get(obj.queue_name, "info")
|
|
43
|
+
return self.html.badge(obj.queue_name, variant=variant, icon=Icons.LAYERS)
|
|
44
|
+
|
|
45
|
+
@computed_field("Status", ordering="status")
|
|
46
|
+
def status_badge(self, obj):
|
|
47
|
+
"""Display status with appropriate badge."""
|
|
48
|
+
variant_map = {
|
|
49
|
+
"queued": "secondary",
|
|
50
|
+
"in_progress": "info",
|
|
51
|
+
"completed": "success",
|
|
52
|
+
"failed": "danger",
|
|
53
|
+
"canceled": "warning",
|
|
54
|
+
}
|
|
55
|
+
icon_map = {
|
|
56
|
+
"queued": Icons.TIMER,
|
|
57
|
+
"in_progress": Icons.SPEED,
|
|
58
|
+
"completed": Icons.CHECK_CIRCLE,
|
|
59
|
+
"failed": Icons.ERROR,
|
|
60
|
+
"canceled": Icons.WARNING,
|
|
61
|
+
}
|
|
62
|
+
variant = variant_map.get(obj.status, "secondary")
|
|
63
|
+
icon = icon_map.get(obj.status, Icons.NOTIFICATIONS)
|
|
64
|
+
return self.html.badge(obj.get_status_display(), variant=variant, icon=icon)
|
|
65
|
+
|
|
66
|
+
@computed_field("Duration", ordering="duration_ms")
|
|
67
|
+
def duration_display(self, obj):
|
|
68
|
+
"""Display duration with color coding based on speed."""
|
|
69
|
+
if obj.duration_ms is None:
|
|
70
|
+
return self.html.empty()
|
|
71
|
+
|
|
72
|
+
# Color code based on duration
|
|
73
|
+
if obj.duration_ms < 1000: # < 1s
|
|
74
|
+
variant = "success" # Fast
|
|
75
|
+
icon = Icons.SPEED
|
|
76
|
+
elif obj.duration_ms < 5000: # < 5s
|
|
77
|
+
variant = "info" # Normal
|
|
78
|
+
icon = Icons.TIMER
|
|
79
|
+
elif obj.duration_ms < 30000: # < 30s
|
|
80
|
+
variant = "warning" # Slow
|
|
81
|
+
icon = Icons.TIMER
|
|
82
|
+
else:
|
|
83
|
+
variant = "danger" # Very slow
|
|
84
|
+
icon = Icons.ERROR
|
|
85
|
+
|
|
86
|
+
# Format duration
|
|
87
|
+
if obj.duration_ms < 1000:
|
|
88
|
+
duration_str = f"{obj.duration_ms}ms"
|
|
89
|
+
else:
|
|
90
|
+
duration_str = f"{obj.duration_ms / 1000:.2f}s"
|
|
91
|
+
|
|
92
|
+
return self.html.badge(duration_str, variant=variant, icon=icon)
|
|
93
|
+
|
|
94
|
+
def args_display(self, obj):
|
|
95
|
+
"""Display formatted JSON arguments."""
|
|
96
|
+
if not obj.args and not obj.kwargs:
|
|
97
|
+
return self.html.empty("No arguments")
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
data = {}
|
|
101
|
+
if obj.args:
|
|
102
|
+
data["args"] = obj.args
|
|
103
|
+
if obj.kwargs:
|
|
104
|
+
data["kwargs"] = obj.kwargs
|
|
105
|
+
|
|
106
|
+
formatted = json.dumps(data, indent=2)
|
|
107
|
+
return f'<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; max-height: 400px; overflow: auto; font-size: 12px; line-height: 1.5;">{formatted}</pre>'
|
|
108
|
+
except Exception:
|
|
109
|
+
return str(data)
|
|
110
|
+
|
|
111
|
+
args_display.short_description = "Task Arguments"
|
|
112
|
+
|
|
113
|
+
def error_details_display(self, obj):
|
|
114
|
+
"""Display error information if task failed."""
|
|
115
|
+
if obj.is_successful or obj.status in ["queued", "in_progress"]:
|
|
116
|
+
return self.html.inline(
|
|
117
|
+
[
|
|
118
|
+
self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
|
|
119
|
+
self.html.span("No errors", "text-green-600"),
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if not obj.error_message:
|
|
124
|
+
return self.html.empty("No error message")
|
|
125
|
+
|
|
126
|
+
return self.html.inline(
|
|
127
|
+
[
|
|
128
|
+
self.html.icon(Icons.ERROR, size="sm"),
|
|
129
|
+
self.html.span(obj.error_message, "text-red-600 font-mono text-sm"),
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
error_details_display.short_description = "Error Details"
|
|
134
|
+
|
|
135
|
+
def retry_info_display(self, obj):
|
|
136
|
+
"""Display retry information."""
|
|
137
|
+
if obj.retry_count == 0:
|
|
138
|
+
return self.html.inline(
|
|
139
|
+
[
|
|
140
|
+
self.html.icon(Icons.CHECK_CIRCLE, size="sm"),
|
|
141
|
+
self.html.span("No retries", "text-gray-600"),
|
|
142
|
+
]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Show retry count with warning if high
|
|
146
|
+
if obj.retry_count >= 3:
|
|
147
|
+
variant = "danger"
|
|
148
|
+
icon = Icons.ERROR
|
|
149
|
+
elif obj.retry_count >= 2:
|
|
150
|
+
variant = "warning"
|
|
151
|
+
icon = Icons.WARNING
|
|
152
|
+
else:
|
|
153
|
+
variant = "info"
|
|
154
|
+
icon = Icons.TIMER
|
|
155
|
+
|
|
156
|
+
return self.html.inline(
|
|
157
|
+
[
|
|
158
|
+
self.html.badge(f"{obj.retry_count} retries", variant=variant, icon=icon),
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
retry_info_display.short_description = "Retry Info"
|
|
163
|
+
|
|
164
|
+
def performance_summary(self, obj):
|
|
165
|
+
"""Display performance summary."""
|
|
166
|
+
stats = []
|
|
167
|
+
|
|
168
|
+
# Duration
|
|
169
|
+
if obj.duration_ms is not None:
|
|
170
|
+
if obj.duration_ms < 1000:
|
|
171
|
+
duration_str = f"{obj.duration_ms}ms"
|
|
172
|
+
else:
|
|
173
|
+
duration_str = f"{obj.duration_ms / 1000:.2f}s"
|
|
174
|
+
stats.append(
|
|
175
|
+
self.html.inline(
|
|
176
|
+
[
|
|
177
|
+
self.html.span("Duration:", "font-semibold"),
|
|
178
|
+
self.html.span(duration_str, "text-gray-600"),
|
|
179
|
+
],
|
|
180
|
+
separator=" ",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Retry count
|
|
185
|
+
if obj.retry_count > 0:
|
|
186
|
+
stats.append(
|
|
187
|
+
self.html.inline(
|
|
188
|
+
[
|
|
189
|
+
self.html.span("Retries:", "font-semibold"),
|
|
190
|
+
self.html.badge(str(obj.retry_count), variant="warning"),
|
|
191
|
+
],
|
|
192
|
+
separator=" ",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Worker ID
|
|
197
|
+
if obj.worker_id:
|
|
198
|
+
stats.append(
|
|
199
|
+
self.html.inline(
|
|
200
|
+
[
|
|
201
|
+
self.html.span("Worker:", "font-semibold"),
|
|
202
|
+
self.html.span(obj.worker_id, "text-gray-600 font-mono text-xs"),
|
|
203
|
+
],
|
|
204
|
+
separator=" ",
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return "<br>".join(stats) if stats else self.html.empty()
|
|
209
|
+
|
|
210
|
+
performance_summary.short_description = "Performance"
|
|
211
|
+
|
|
212
|
+
# Fieldsets for detail view
|
|
213
|
+
def get_fieldsets(self, request, obj=None):
|
|
214
|
+
"""Dynamic fieldsets based on object state."""
|
|
215
|
+
fieldsets = [
|
|
216
|
+
(
|
|
217
|
+
"Task Information",
|
|
218
|
+
{
|
|
219
|
+
"fields": (
|
|
220
|
+
"id",
|
|
221
|
+
"job_id",
|
|
222
|
+
"task_name",
|
|
223
|
+
"queue_name",
|
|
224
|
+
"status",
|
|
225
|
+
"user",
|
|
226
|
+
)
|
|
227
|
+
},
|
|
228
|
+
),
|
|
229
|
+
(
|
|
230
|
+
"Arguments",
|
|
231
|
+
{"fields": ("args_display",), "classes": ("collapse",)},
|
|
232
|
+
),
|
|
233
|
+
(
|
|
234
|
+
"Performance",
|
|
235
|
+
{
|
|
236
|
+
"fields": (
|
|
237
|
+
"performance_summary",
|
|
238
|
+
"created_at",
|
|
239
|
+
"started_at",
|
|
240
|
+
"completed_at",
|
|
241
|
+
)
|
|
242
|
+
},
|
|
243
|
+
),
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
# Add error section only if failed
|
|
247
|
+
if obj and obj.is_failed:
|
|
248
|
+
fieldsets.insert(
|
|
249
|
+
2,
|
|
250
|
+
(
|
|
251
|
+
"Error Details",
|
|
252
|
+
{
|
|
253
|
+
"fields": (
|
|
254
|
+
"error_details_display",
|
|
255
|
+
"error_message",
|
|
256
|
+
"retry_info_display",
|
|
257
|
+
)
|
|
258
|
+
},
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return fieldsets
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
__all__ = ["TaskLogAdmin"]
|
django_cfg/apps/tasks/apps.py
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Django CFG Tasks App Configuration
|
|
3
|
-
"""
|
|
4
|
-
|
|
1
|
+
"""Django AppConfig for tasks app."""
|
|
5
2
|
from django.apps import AppConfig
|
|
6
3
|
|
|
7
4
|
|
|
8
5
|
class TasksConfig(AppConfig):
|
|
9
|
-
"""
|
|
6
|
+
"""Django app configuration for ReArq tasks."""
|
|
10
7
|
|
|
11
|
-
default_auto_field =
|
|
12
|
-
name =
|
|
13
|
-
verbose_name =
|
|
8
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
9
|
+
name = "django_cfg.apps.tasks"
|
|
10
|
+
verbose_name = "Background Tasks"
|
|
14
11
|
|
|
15
12
|
def ready(self):
|
|
16
13
|
"""Initialize app when Django starts."""
|
|
17
|
-
|
|
14
|
+
# Import services to ensure client is available
|
|
15
|
+
from . import services # noqa: F401
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TaskLog FilterSet for advanced filtering.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive filtering capabilities for task logs.
|
|
5
|
+
"""
|
|
6
|
+
import django_filters
|
|
7
|
+
from django.db.models import Q
|
|
8
|
+
|
|
9
|
+
from ..models import TaskLog
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TaskLogFilter(django_filters.FilterSet):
|
|
13
|
+
"""
|
|
14
|
+
Advanced FilterSet for TaskLog.
|
|
15
|
+
|
|
16
|
+
Matches ReArq API query parameters where possible.
|
|
17
|
+
|
|
18
|
+
Provides filters for:
|
|
19
|
+
- Task name (exact, contains)
|
|
20
|
+
- Queue name (exact, in)
|
|
21
|
+
- Status (exact, in) - matching ReArq JobStatus
|
|
22
|
+
- Date ranges (enqueue, start, finish)
|
|
23
|
+
- Duration ranges
|
|
24
|
+
- Retry count ranges
|
|
25
|
+
- Success/failure flags
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Task name filters (matching ReArq "task" param)
|
|
29
|
+
task = django_filters.CharFilter(field_name='task_name', lookup_expr='exact')
|
|
30
|
+
task_name = django_filters.CharFilter(lookup_expr='icontains')
|
|
31
|
+
task_name_exact = django_filters.CharFilter(field_name='task_name', lookup_expr='exact')
|
|
32
|
+
|
|
33
|
+
# Queue filters
|
|
34
|
+
queue_name = django_filters.CharFilter(lookup_expr='exact')
|
|
35
|
+
queue_name_in = django_filters.BaseInFilter(field_name='queue_name')
|
|
36
|
+
|
|
37
|
+
# Status filters (matching ReArq "status" param)
|
|
38
|
+
status = django_filters.CharFilter(lookup_expr='exact')
|
|
39
|
+
status_in = django_filters.BaseInFilter(field_name='status')
|
|
40
|
+
|
|
41
|
+
# Success filter (matching ReArq JobResult)
|
|
42
|
+
success = django_filters.BooleanFilter()
|
|
43
|
+
|
|
44
|
+
# Boolean flags (computed properties)
|
|
45
|
+
is_completed = django_filters.BooleanFilter(method='filter_is_completed')
|
|
46
|
+
is_successful = django_filters.BooleanFilter(method='filter_is_successful')
|
|
47
|
+
is_failed = django_filters.BooleanFilter(method='filter_is_failed')
|
|
48
|
+
|
|
49
|
+
# Date range filters (matching ReArq "start_time" / "end_time")
|
|
50
|
+
start_time = django_filters.DateTimeFilter(field_name='enqueue_time', lookup_expr='gte')
|
|
51
|
+
end_time = django_filters.DateTimeFilter(field_name='enqueue_time', lookup_expr='lte')
|
|
52
|
+
|
|
53
|
+
# Additional date filters
|
|
54
|
+
enqueue_after = django_filters.DateTimeFilter(field_name='enqueue_time', lookup_expr='gte')
|
|
55
|
+
enqueue_before = django_filters.DateTimeFilter(field_name='enqueue_time', lookup_expr='lte')
|
|
56
|
+
|
|
57
|
+
start_after = django_filters.DateTimeFilter(field_name='start_time', lookup_expr='gte')
|
|
58
|
+
start_before = django_filters.DateTimeFilter(field_name='start_time', lookup_expr='lte')
|
|
59
|
+
|
|
60
|
+
finish_after = django_filters.DateTimeFilter(field_name='finish_time', lookup_expr='gte')
|
|
61
|
+
finish_before = django_filters.DateTimeFilter(field_name='finish_time', lookup_expr='lte')
|
|
62
|
+
|
|
63
|
+
# Django-specific filters
|
|
64
|
+
created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
|
|
65
|
+
created_before = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
|
|
66
|
+
|
|
67
|
+
# Duration filters (milliseconds)
|
|
68
|
+
duration_min = django_filters.NumberFilter(field_name='duration_ms', lookup_expr='gte')
|
|
69
|
+
duration_max = django_filters.NumberFilter(field_name='duration_ms', lookup_expr='lte')
|
|
70
|
+
|
|
71
|
+
# Retry filters (matching ReArq Job fields)
|
|
72
|
+
job_retries_min = django_filters.NumberFilter(field_name='job_retries', lookup_expr='gte')
|
|
73
|
+
job_retries_max = django_filters.NumberFilter(field_name='job_retries', lookup_expr='lte')
|
|
74
|
+
|
|
75
|
+
# Job ID filter (matching ReArq "job_id" param)
|
|
76
|
+
job_id = django_filters.CharFilter(lookup_expr='exact')
|
|
77
|
+
|
|
78
|
+
# Worker filter (matching ReArq JobResult "worker" param)
|
|
79
|
+
worker = django_filters.CharFilter(field_name='worker_id', lookup_expr='exact')
|
|
80
|
+
|
|
81
|
+
# Error message search
|
|
82
|
+
has_error = django_filters.BooleanFilter(method='filter_has_error')
|
|
83
|
+
|
|
84
|
+
class Meta:
|
|
85
|
+
model = TaskLog
|
|
86
|
+
fields = [
|
|
87
|
+
'task_name',
|
|
88
|
+
'queue_name',
|
|
89
|
+
'status',
|
|
90
|
+
'success',
|
|
91
|
+
'job_id',
|
|
92
|
+
'worker_id',
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
def filter_has_error(self, queryset, name, value):
|
|
96
|
+
"""Filter tasks with/without errors."""
|
|
97
|
+
if value:
|
|
98
|
+
return queryset.filter(error_message__isnull=False).exclude(error_message='')
|
|
99
|
+
else:
|
|
100
|
+
return queryset.filter(Q(error_message__isnull=True) | Q(error_message=''))
|
|
101
|
+
|
|
102
|
+
def filter_is_completed(self, queryset, name, value):
|
|
103
|
+
"""Filter by completed status."""
|
|
104
|
+
if value:
|
|
105
|
+
return queryset.filter(status__in=['success', 'failed', 'expired', 'canceled'])
|
|
106
|
+
else:
|
|
107
|
+
return queryset.filter(status__in=['deferred', 'queued', 'in_progress'])
|
|
108
|
+
|
|
109
|
+
def filter_is_successful(self, queryset, name, value):
|
|
110
|
+
"""Filter by successful completion."""
|
|
111
|
+
if value:
|
|
112
|
+
return queryset.filter(status='success', success=True)
|
|
113
|
+
else:
|
|
114
|
+
return queryset.exclude(status='success', success=True)
|
|
115
|
+
|
|
116
|
+
def filter_is_failed(self, queryset, name, value):
|
|
117
|
+
"""Filter by failed status."""
|
|
118
|
+
if value:
|
|
119
|
+
return queryset.filter(Q(status__in=['failed', 'expired']) | Q(success=False))
|
|
120
|
+
else:
|
|
121
|
+
return queryset.exclude(Q(status__in=['failed', 'expired']) | Q(success=False))
|