django-celeryx 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. django_celeryx-0.1.0a1/.gitignore +35 -0
  2. django_celeryx-0.1.0a1/LICENSE +21 -0
  3. django_celeryx-0.1.0a1/PKG-INFO +148 -0
  4. django_celeryx-0.1.0a1/README.md +106 -0
  5. django_celeryx-0.1.0a1/django_celeryx/__init__.py +10 -0
  6. django_celeryx-0.1.0a1/django_celeryx/admin/__init__.py +1 -0
  7. django_celeryx-0.1.0a1/django_celeryx/admin/admin.py +331 -0
  8. django_celeryx-0.1.0a1/django_celeryx/admin/apps.py +52 -0
  9. django_celeryx-0.1.0a1/django_celeryx/admin/helpers.py +5 -0
  10. django_celeryx-0.1.0a1/django_celeryx/admin/migrations/0001_initial.py +148 -0
  11. django_celeryx-0.1.0a1/django_celeryx/admin/migrations/0002_remove_taskevent_celeryx_task_uuid_idx_and_more.py +34 -0
  12. django_celeryx-0.1.0a1/django_celeryx/admin/migrations/0003_rename_event_to_state.py +20 -0
  13. django_celeryx-0.1.0a1/django_celeryx/admin/migrations/__init__.py +0 -0
  14. django_celeryx-0.1.0a1/django_celeryx/admin/models.py +163 -0
  15. django_celeryx-0.1.0a1/django_celeryx/admin/queryset.py +815 -0
  16. django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/change_list.html +62 -0
  17. django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/dashboard/change_list.html +281 -0
  18. django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/task/apply.html +70 -0
  19. django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/task/change_form.html +186 -0
  20. django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/task/change_list.html +9 -0
  21. django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/worker/change_form.html +352 -0
  22. django_celeryx-0.1.0a1/django_celeryx/admin/templates/admin/django_celeryx/worker/change_list.html +34 -0
  23. django_celeryx-0.1.0a1/django_celeryx/admin/templatetags/__init__.py +0 -0
  24. django_celeryx-0.1.0a1/django_celeryx/admin/templatetags/celeryx_tags.py +59 -0
  25. django_celeryx-0.1.0a1/django_celeryx/admin/views/__init__.py +1 -0
  26. django_celeryx-0.1.0a1/django_celeryx/admin/views/apply_task.py +98 -0
  27. django_celeryx-0.1.0a1/django_celeryx/admin/views/dashboard.py +191 -0
  28. django_celeryx-0.1.0a1/django_celeryx/admin/views/task_detail.py +95 -0
  29. django_celeryx-0.1.0a1/django_celeryx/admin/views/worker_detail.py +217 -0
  30. django_celeryx-0.1.0a1/django_celeryx/apps.py +5 -0
  31. django_celeryx-0.1.0a1/django_celeryx/control/__init__.py +1 -0
  32. django_celeryx-0.1.0a1/django_celeryx/control/tasks.py +59 -0
  33. django_celeryx-0.1.0a1/django_celeryx/control/workers.py +63 -0
  34. django_celeryx-0.1.0a1/django_celeryx/db_models.py +78 -0
  35. django_celeryx-0.1.0a1/django_celeryx/db_router.py +49 -0
  36. django_celeryx-0.1.0a1/django_celeryx/helpers.py +25 -0
  37. django_celeryx-0.1.0a1/django_celeryx/metrics.py +172 -0
  38. django_celeryx-0.1.0a1/django_celeryx/py.typed +0 -0
  39. django_celeryx-0.1.0a1/django_celeryx/settings.py +149 -0
  40. django_celeryx-0.1.0a1/django_celeryx/state/__init__.py +1 -0
  41. django_celeryx-0.1.0a1/django_celeryx/state/events.py +301 -0
  42. django_celeryx-0.1.0a1/django_celeryx/state/persistence.py +146 -0
  43. django_celeryx-0.1.0a1/django_celeryx/types.py +43 -0
  44. django_celeryx-0.1.0a1/django_celeryx/unfold/__init__.py +1 -0
  45. django_celeryx-0.1.0a1/django_celeryx/unfold/admin.py +244 -0
  46. django_celeryx-0.1.0a1/django_celeryx/unfold/apps.py +29 -0
  47. django_celeryx-0.1.0a1/django_celeryx/unfold/models.py +5 -0
  48. django_celeryx-0.1.0a1/django_celeryx/urls.py +25 -0
  49. django_celeryx-0.1.0a1/pyproject.toml +220 -0
@@ -0,0 +1,35 @@
1
+ *.py[cod]
2
+ __pycache__/
3
+ .DS_Store
4
+ *.sql
5
+ *.bz2
6
+ *~
7
+ *.log
8
+ *.json
9
+ *.wsgi
10
+ local_settings.py
11
+ development_settings.py
12
+ *.egg-info
13
+ .project
14
+ .pydevproject
15
+ .settings
16
+ versiontools*
17
+ _build*
18
+ doc/index.html
19
+ /build/
20
+ /dist/
21
+ *.swp
22
+ \#*
23
+ .\#*
24
+ .tox
25
+ dump.rdb
26
+ .idea
27
+ .venv
28
+ .coverage
29
+ coverage.xml
30
+ cobertura.xml
31
+ CLAUDE.md
32
+ site/
33
+ *.sqlite3
34
+ *.sqlite3-shm
35
+ *.sqlite3-wal
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oliver Haas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-celeryx
3
+ Version: 0.1.0a1
4
+ Summary: Celery monitoring and management for Django admin
5
+ Project-URL: Homepage, https://github.com/oliverhaas/django-celeryx
6
+ Project-URL: Documentation, https://oliverhaas.github.io/django-celeryx/
7
+ Project-URL: Repository, https://github.com/oliverhaas/django-celeryx.git
8
+ Project-URL: Changelog, https://oliverhaas.github.io/django-celeryx/reference/changelog/
9
+ Author-email: Oliver Haas <ohaas@e1plus.de>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: admin,celery,django,flower,monitoring
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Django :: 5.2
17
+ Classifier: Framework :: Django :: 6.0
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python
22
+ Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3 :: Only
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3.14
27
+ Classifier: Topic :: Internet :: WWW/HTTP
28
+ Classifier: Topic :: Software Development :: Libraries
29
+ Classifier: Topic :: System :: Monitoring
30
+ Classifier: Typing :: Typed
31
+ Requires-Python: >=3.12
32
+ Requires-Dist: django<7,>=5.2
33
+ Provides-Extra: celery
34
+ Requires-Dist: celery<6,>=5.4; extra == 'celery'
35
+ Provides-Extra: celery-asyncio
36
+ Requires-Dist: celery-asyncio>=6.0.0a1; extra == 'celery-asyncio'
37
+ Provides-Extra: prometheus
38
+ Requires-Dist: prometheus-client>=0.20.0; extra == 'prometheus'
39
+ Provides-Extra: unfold
40
+ Requires-Dist: django-unfold>=0.70.0; extra == 'unfold'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # django-celeryx
44
+
45
+ [![PyPI version](https://img.shields.io/pypi/v/django-celeryx.svg?style=flat)](https://pypi.org/project/django-celeryx/)
46
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-celeryx.svg)](https://pypi.org/project/django-celeryx/)
47
+ [![CI](https://github.com/oliverhaas/django-celeryx/actions/workflows/ci.yml/badge.svg)](https://github.com/oliverhaas/django-celeryx/actions/workflows/ci.yml)
48
+
49
+ Celery monitoring and management for Django admin. Like Flower, but embedded in your Django admin with htmx for real-time updates.
50
+
51
+ ## Installation
52
+
53
+ ```console
54
+ pip install django-celeryx[celery]
55
+ ```
56
+
57
+ Or with celery-asyncio:
58
+
59
+ ```console
60
+ pip install django-celeryx[celery-asyncio]
61
+ ```
62
+
63
+ ## Quick Start
64
+
65
+ ```python
66
+ INSTALLED_APPS = [
67
+ # ...
68
+ "django_celeryx.admin",
69
+ ]
70
+ ```
71
+
72
+ That's it. Start your Django server and navigate to the admin to see your Celery tasks and workers.
73
+
74
+ ## Features
75
+
76
+ - **Real-time task monitoring** — Live task list with state, args, result, timing, auto-refreshing via htmx
77
+ - **Worker management** — View worker status, pool info, active queues, configuration
78
+ - **Control actions** — Revoke/terminate tasks, shutdown/restart workers, manage pools and queues
79
+ - **Broker overview** — Queue names, routing keys, consumer counts
80
+ - **Django admin native** — Looks and feels like standard Django admin, no separate service to run
81
+ - **Database persistence** — All state persisted to database (dedicated SQLite file by default, or any Django database)
82
+ - **Registered tasks** — Browse all registered task types, link to filtered task list
83
+
84
+ ## Task Monitoring
85
+
86
+ The task list shows all Celery tasks with color-coded states:
87
+
88
+ - **PENDING** (grey), **RECEIVED** (yellow), **STARTED** (blue), **SUCCESS** (green), **FAILURE** (red), **RETRY** (orange), **REVOKED** (purple)
89
+
90
+ Configurable columns via `TASK_COLUMNS` setting.
91
+
92
+ ## Worker Management
93
+
94
+ Worker detail view with tabbed interface (matching Flower):
95
+
96
+ - **Pool** — Pool type, concurrency, processes. Controls: grow/shrink, autoscale
97
+ - **Queues** — Active queues. Controls: add/cancel consumer
98
+ - **Tasks** — Processed counts, active/scheduled/reserved/revoked tasks
99
+ - **Limits** — Rate limits and timeouts
100
+ - **Config** — Full worker Celery configuration
101
+ - **Stats** — System resource usage, broker connection info
102
+
103
+ ## Control Actions
104
+
105
+ Full control parity with Flower:
106
+
107
+ - Revoke / terminate / abort tasks
108
+ - Shutdown / restart worker pool
109
+ - Grow / shrink pool, set autoscale
110
+ - Add / cancel queue consumer
111
+ - Set rate limits and time limits
112
+
113
+ ## Configuration
114
+
115
+ ```python
116
+ CELERYX = {
117
+ "MAX_TASK_COUNT": 100_000,
118
+ "MAX_TASK_AGE": 86400, # 24 hours
119
+ "AUTO_REFRESH_INTERVAL": 3,
120
+ "TASK_COLUMNS": ["name", "uuid", "state", "worker", "received", "started", "runtime"],
121
+ }
122
+ ```
123
+
124
+ ## Unfold Theme
125
+
126
+ For [django-unfold](https://github.com/unfoldadmin/django-unfold) users:
127
+
128
+ ```python
129
+ INSTALLED_APPS = [
130
+ "unfold",
131
+ # ...
132
+ "django_celeryx.unfold", # instead of django_celeryx.admin
133
+ ]
134
+ ```
135
+
136
+ ## Documentation
137
+
138
+ Full documentation at [oliverhaas.github.io/django-celeryx](https://oliverhaas.github.io/django-celeryx/)
139
+
140
+ ## Requirements
141
+
142
+ - Python 3.12+
143
+ - Django 5.2+
144
+ - Celery 5.4+ or celery-asyncio 6.0+
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,106 @@
1
+ # django-celeryx
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/django-celeryx.svg?style=flat)](https://pypi.org/project/django-celeryx/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-celeryx.svg)](https://pypi.org/project/django-celeryx/)
5
+ [![CI](https://github.com/oliverhaas/django-celeryx/actions/workflows/ci.yml/badge.svg)](https://github.com/oliverhaas/django-celeryx/actions/workflows/ci.yml)
6
+
7
+ Celery monitoring and management for Django admin. Like Flower, but embedded in your Django admin with htmx for real-time updates.
8
+
9
+ ## Installation
10
+
11
+ ```console
12
+ pip install django-celeryx[celery]
13
+ ```
14
+
15
+ Or with celery-asyncio:
16
+
17
+ ```console
18
+ pip install django-celeryx[celery-asyncio]
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```python
24
+ INSTALLED_APPS = [
25
+ # ...
26
+ "django_celeryx.admin",
27
+ ]
28
+ ```
29
+
30
+ That's it. Start your Django server and navigate to the admin to see your Celery tasks and workers.
31
+
32
+ ## Features
33
+
34
+ - **Real-time task monitoring** — Live task list with state, args, result, timing, auto-refreshing via htmx
35
+ - **Worker management** — View worker status, pool info, active queues, configuration
36
+ - **Control actions** — Revoke/terminate tasks, shutdown/restart workers, manage pools and queues
37
+ - **Broker overview** — Queue names, routing keys, consumer counts
38
+ - **Django admin native** — Looks and feels like standard Django admin, no separate service to run
39
+ - **Database persistence** — All state persisted to database (dedicated SQLite file by default, or any Django database)
40
+ - **Registered tasks** — Browse all registered task types, link to filtered task list
41
+
42
+ ## Task Monitoring
43
+
44
+ The task list shows all Celery tasks with color-coded states:
45
+
46
+ - **PENDING** (grey), **RECEIVED** (yellow), **STARTED** (blue), **SUCCESS** (green), **FAILURE** (red), **RETRY** (orange), **REVOKED** (purple)
47
+
48
+ Configurable columns via `TASK_COLUMNS` setting.
49
+
50
+ ## Worker Management
51
+
52
+ Worker detail view with tabbed interface (matching Flower):
53
+
54
+ - **Pool** — Pool type, concurrency, processes. Controls: grow/shrink, autoscale
55
+ - **Queues** — Active queues. Controls: add/cancel consumer
56
+ - **Tasks** — Processed counts, active/scheduled/reserved/revoked tasks
57
+ - **Limits** — Rate limits and timeouts
58
+ - **Config** — Full worker Celery configuration
59
+ - **Stats** — System resource usage, broker connection info
60
+
61
+ ## Control Actions
62
+
63
+ Full control parity with Flower:
64
+
65
+ - Revoke / terminate / abort tasks
66
+ - Shutdown / restart worker pool
67
+ - Grow / shrink pool, set autoscale
68
+ - Add / cancel queue consumer
69
+ - Set rate limits and time limits
70
+
71
+ ## Configuration
72
+
73
+ ```python
74
+ CELERYX = {
75
+ "MAX_TASK_COUNT": 100_000,
76
+ "MAX_TASK_AGE": 86400, # 24 hours
77
+ "AUTO_REFRESH_INTERVAL": 3,
78
+ "TASK_COLUMNS": ["name", "uuid", "state", "worker", "received", "started", "runtime"],
79
+ }
80
+ ```
81
+
82
+ ## Unfold Theme
83
+
84
+ For [django-unfold](https://github.com/unfoldadmin/django-unfold) users:
85
+
86
+ ```python
87
+ INSTALLED_APPS = [
88
+ "unfold",
89
+ # ...
90
+ "django_celeryx.unfold", # instead of django_celeryx.admin
91
+ ]
92
+ ```
93
+
94
+ ## Documentation
95
+
96
+ Full documentation at [oliverhaas.github.io/django-celeryx](https://oliverhaas.github.io/django-celeryx/)
97
+
98
+ ## Requirements
99
+
100
+ - Python 3.12+
101
+ - Django 5.2+
102
+ - Celery 5.4+ or celery-asyncio 6.0+
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,10 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("django-celeryx")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0+unknown"
7
+
8
+ __all__ = [
9
+ "__version__",
10
+ ]
@@ -0,0 +1 @@
1
+ """Admin interface for Celery monitoring and management."""
@@ -0,0 +1,331 @@
1
+ """Django admin classes for Celery monitoring and management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from django.contrib import admin, messages
8
+ from django.contrib.admin.utils import unquote
9
+ from django.core.exceptions import PermissionDenied
10
+ from django.urls import path
11
+ from django.utils.translation import gettext_lazy as _
12
+
13
+ from .models import Dashboard, Queue, RegisteredTask, Task, Worker
14
+ from .queryset import (
15
+ QueueAdminMixin,
16
+ RegisteredTaskAdminMixin,
17
+ TaskAdminMixin,
18
+ WorkerAdminMixin,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from django.http import HttpRequest, HttpResponse
23
+
24
+ _TaskBase = admin.ModelAdmin[Task]
25
+ _WorkerBase = admin.ModelAdmin[Worker]
26
+ _QueueBase = admin.ModelAdmin[Queue]
27
+ _RegisteredTaskBase = admin.ModelAdmin[RegisteredTask]
28
+ else:
29
+ _TaskBase = admin.ModelAdmin
30
+ _WorkerBase = admin.ModelAdmin
31
+ _QueueBase = admin.ModelAdmin
32
+ _RegisteredTaskBase = admin.ModelAdmin
33
+
34
+
35
+ class LiveUpdateMixin:
36
+ """Mixin that adds htmx live update toggle to changelist views."""
37
+
38
+ change_list_template = "admin/django_celeryx/change_list.html"
39
+
40
+ def changelist_view(
41
+ self,
42
+ request: HttpRequest,
43
+ extra_context: dict[str, Any] | None = None,
44
+ ) -> HttpResponse:
45
+ from django_celeryx.settings import celeryx_settings
46
+
47
+ extra_context = extra_context or {}
48
+
49
+ live = request.GET.get("live") == "on"
50
+ extra_context["live"] = live
51
+ extra_context["refresh_interval"] = celeryx_settings.AUTO_REFRESH_INTERVAL
52
+
53
+ params = request.GET.copy()
54
+ if live:
55
+ params.pop("live", None)
56
+ else:
57
+ params["live"] = "on"
58
+ toggle_qs = params.urlencode()
59
+ extra_context["live_toggle_url"] = f"?{toggle_qs}" if toggle_qs else "?"
60
+ extra_context["live_url"] = request.get_full_path()
61
+
62
+ # Strip 'live' from request.GET so ChangeList doesn't treat it as a filter.
63
+ # Also update QUERY_STRING so get_full_path() stays consistent.
64
+ if "live" in request.GET:
65
+ request.GET = request.GET.copy() # type: ignore[assignment]
66
+ request.GET.pop("live") # type: ignore[misc]
67
+ request.META["QUERY_STRING"] = request.GET.urlencode()
68
+
69
+ return super().changelist_view(request, extra_context) # type: ignore[misc]
70
+
71
+
72
+ @admin.register(Task)
73
+ class TaskAdmin(LiveUpdateMixin, TaskAdminMixin, _TaskBase): # type: ignore[misc]
74
+ """Admin for Celery tasks."""
75
+
76
+ change_list_template = "admin/django_celeryx/task/change_list.html" # type: ignore[misc]
77
+ actions = ["revoke_selected", "terminate_selected"] # noqa: RUF012
78
+
79
+ def has_add_permission(self, request: HttpRequest) -> bool:
80
+ return False
81
+
82
+ def has_delete_permission(self, request: HttpRequest, obj: Task | None = None) -> bool:
83
+ return False
84
+
85
+ def get_urls(self) -> list:
86
+ urls = super().get_urls()
87
+ custom_urls = [
88
+ path(
89
+ "apply/",
90
+ self.admin_site.admin_view(self._apply_task_view),
91
+ name="django_celeryx_task_apply",
92
+ ),
93
+ path(
94
+ "<path:object_id>/change/",
95
+ self.admin_site.admin_view(self.change_view),
96
+ name="django_celeryx_task_change",
97
+ ),
98
+ ]
99
+ return custom_urls + urls
100
+
101
+ def _apply_task_view(self, request: HttpRequest) -> HttpResponse:
102
+ from .views.apply_task import apply_task_view
103
+
104
+ return apply_task_view(request)
105
+
106
+ def change_view(
107
+ self,
108
+ request: HttpRequest,
109
+ object_id: str,
110
+ form_url: str = "",
111
+ extra_context: dict[str, Any] | None = None,
112
+ ) -> HttpResponse:
113
+ if not self.has_view_or_change_permission(request):
114
+ raise PermissionDenied
115
+
116
+ from .views.task_detail import task_detail_view
117
+
118
+ return task_detail_view(request, unquote(object_id))
119
+
120
+ @admin.action(description=_("Revoke selected tasks"))
121
+ def revoke_selected(self, request: HttpRequest, queryset: Any) -> None:
122
+ from django_celeryx.control.tasks import revoke_task
123
+
124
+ count = 0
125
+ for task in queryset:
126
+ try:
127
+ revoke_task(task.uuid)
128
+ count += 1
129
+ except Exception as exc:
130
+ messages.error(request, f"Failed to revoke {task.uuid[:8]}: {exc}")
131
+ if count:
132
+ messages.success(request, f"Revoked {count} task(s).")
133
+
134
+ @admin.action(description=_("Terminate selected tasks"))
135
+ def terminate_selected(self, request: HttpRequest, queryset: Any) -> None:
136
+ from django_celeryx.control.tasks import revoke_task
137
+
138
+ count = 0
139
+ for task in queryset:
140
+ try:
141
+ revoke_task(task.uuid, terminate=True)
142
+ count += 1
143
+ except Exception as exc:
144
+ messages.error(request, f"Failed to terminate {task.uuid[:8]}: {exc}")
145
+ if count:
146
+ messages.success(request, f"Terminated {count} task(s).")
147
+
148
+
149
+ @admin.register(Worker)
150
+ class WorkerAdmin(LiveUpdateMixin, WorkerAdminMixin, _WorkerBase): # type: ignore[misc]
151
+ """Admin for Celery workers."""
152
+
153
+ change_list_template = "admin/django_celeryx/worker/change_list.html" # type: ignore[misc]
154
+
155
+ def has_add_permission(self, request: HttpRequest) -> bool:
156
+ return False
157
+
158
+ def has_delete_permission(self, request: HttpRequest, obj: Worker | None = None) -> bool:
159
+ return False
160
+
161
+ def get_urls(self) -> list:
162
+ urls = super().get_urls()
163
+ custom_urls = [
164
+ path(
165
+ "<path:object_id>/change/",
166
+ self.admin_site.admin_view(self.change_view),
167
+ name="django_celeryx_worker_change",
168
+ ),
169
+ ]
170
+ return custom_urls + urls
171
+
172
+ def change_view(
173
+ self,
174
+ request: HttpRequest,
175
+ object_id: str,
176
+ form_url: str = "",
177
+ extra_context: dict[str, Any] | None = None,
178
+ ) -> HttpResponse:
179
+ if not self.has_view_or_change_permission(request):
180
+ raise PermissionDenied
181
+
182
+ from .views.worker_detail import worker_detail_view
183
+
184
+ return worker_detail_view(request, unquote(object_id))
185
+
186
+
187
+ @admin.register(Queue)
188
+ class QueueAdmin(LiveUpdateMixin, QueueAdminMixin, _QueueBase): # type: ignore[misc]
189
+ """Admin for Celery queues."""
190
+
191
+ def has_add_permission(self, request: HttpRequest) -> bool:
192
+ return False
193
+
194
+ def has_delete_permission(self, request: HttpRequest, obj: Queue | None = None) -> bool:
195
+ return False
196
+
197
+
198
+ @admin.register(RegisteredTask)
199
+ class RegisteredTaskAdmin(RegisteredTaskAdminMixin, _RegisteredTaskBase): # type: ignore[misc]
200
+ """Admin for registered Celery task types."""
201
+
202
+ def has_add_permission(self, request: HttpRequest) -> bool:
203
+ return False
204
+
205
+ def has_delete_permission(self, request: HttpRequest, obj: RegisteredTask | None = None) -> bool:
206
+ return False
207
+
208
+
209
+ class DashboardPeriodFilter(admin.SimpleListFilter):
210
+ title = _("time period")
211
+ parameter_name = "period"
212
+
213
+ def lookups(self, request: HttpRequest, model_admin: admin.ModelAdmin) -> list[tuple[str, str]]:
214
+ return [("today", str(_("Today"))), ("7d", str(_("Last 7 days"))), ("30d", str(_("Last 30 days")))]
215
+
216
+ def queryset(self, request: HttpRequest, queryset: Any) -> Any:
217
+ deltas = {"today": 1, "7d": 7, "30d": 30}
218
+ value = self.value()
219
+ if value is not None and value in deltas:
220
+ import time
221
+
222
+ cutoff = time.time() - deltas[value] * 86400
223
+ return queryset.filter(updated_at__gte=cutoff)
224
+ return queryset
225
+
226
+
227
+ class DashboardQueueFilter(admin.SimpleListFilter):
228
+ title = _("queue")
229
+ parameter_name = "queue"
230
+
231
+ def lookups(self, request: HttpRequest, model_admin: admin.ModelAdmin) -> list[tuple[str, str]]:
232
+ from django_celeryx.db_models import TaskState
233
+ from django_celeryx.settings import get_db_alias
234
+
235
+ try:
236
+ return [
237
+ (q, q)
238
+ for q in sorted(
239
+ TaskState.objects.using(get_db_alias())
240
+ .exclude(routing_key="")
241
+ .values_list("routing_key", flat=True)
242
+ .distinct()
243
+ )
244
+ ]
245
+ except Exception:
246
+ return []
247
+
248
+ def queryset(self, request: HttpRequest, queryset: Any) -> Any:
249
+ if self.value():
250
+ return queryset.filter(routing_key=self.value())
251
+ return queryset
252
+
253
+
254
+ class DashboardWorkerFilter(admin.SimpleListFilter):
255
+ title = _("worker")
256
+ parameter_name = "worker"
257
+
258
+ def lookups(self, request: HttpRequest, model_admin: admin.ModelAdmin) -> list[tuple[str, str]]:
259
+ from django_celeryx.db_models import TaskState
260
+ from django_celeryx.settings import get_db_alias
261
+
262
+ try:
263
+ return [
264
+ (w, w)
265
+ for w in sorted(
266
+ TaskState.objects.using(get_db_alias())
267
+ .exclude(worker="")
268
+ .values_list("worker", flat=True)
269
+ .distinct()
270
+ )
271
+ ]
272
+ except Exception:
273
+ return []
274
+
275
+ def queryset(self, request: HttpRequest, queryset: Any) -> Any:
276
+ if self.value():
277
+ return queryset.filter(worker=self.value())
278
+ return queryset
279
+
280
+
281
+ if TYPE_CHECKING:
282
+ _DashboardBase = admin.ModelAdmin[Dashboard]
283
+ else:
284
+ _DashboardBase = admin.ModelAdmin
285
+
286
+
287
+ @admin.register(Dashboard)
288
+ class DashboardAdmin(LiveUpdateMixin, _DashboardBase):
289
+ """Dashboard view using native Django admin filters."""
290
+
291
+ change_list_template = "admin/django_celeryx/dashboard/change_list.html" # type: ignore[misc]
292
+ list_filter = [DashboardPeriodFilter, DashboardQueueFilter, DashboardWorkerFilter] # noqa: RUF012
293
+ show_facets = admin.ShowFacets.NEVER
294
+
295
+ def has_add_permission(self, request: HttpRequest) -> bool:
296
+ return False
297
+
298
+ def has_delete_permission(self, request: HttpRequest, obj: Dashboard | None = None) -> bool:
299
+ return False
300
+
301
+ def has_change_permission(self, request: HttpRequest, obj: Dashboard | None = None) -> bool:
302
+ return False
303
+
304
+ def get_queryset(self, request: HttpRequest) -> Any:
305
+ # Return empty qs so ChangeList never queries the unmanaged Dashboard table.
306
+ # Actual data is computed manually in changelist_view.
307
+ from django_celeryx.db_models import TaskState
308
+
309
+ return TaskState.objects.none()
310
+
311
+ def changelist_view(
312
+ self,
313
+ request: HttpRequest,
314
+ extra_context: dict[str, Any] | None = None,
315
+ ) -> HttpResponse:
316
+ extra_context = extra_context or {}
317
+
318
+ # Build a filtered queryset by applying our filters to TaskState
319
+ from django_celeryx.db_models import TaskState as _TaskState
320
+ from django_celeryx.settings import get_db_alias
321
+
322
+ from .views.dashboard import compute_dashboard_context
323
+
324
+ qs = _TaskState.objects.using(get_db_alias()).all()
325
+ for f_cls in self.list_filter:
326
+ f = f_cls(request, request.GET.copy(), _TaskState, self) # type: ignore[operator]
327
+ qs = f.queryset(request, qs) or qs
328
+
329
+ extra_context.update(compute_dashboard_context(qs, period=request.GET.get("period", "")))
330
+
331
+ return super().changelist_view(request, extra_context)