Skip to content

Commit 7b8f7f9

Browse files
authored
Automatically add Django RQ views to admin (#747)
* By default show Django-RQ link in admin sidebar * Automatically add DjangoRQ views to Django's admin inteface * Improve test_admin_integration.py * Delete unused variable * Split AdminURLIntegrationTest into two different test cases * Check for invalid API calls by asserting status codes * Changed the dashboard URL from /queue to /dashboard * Updated README.md
1 parent 3bddb93 commit 7b8f7f9

File tree

6 files changed

+306
-49
lines changed

6 files changed

+306
-49
lines changed

README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,30 @@ RQ_QUEUES = {
7777
RQ_EXCEPTION_HANDLERS = ['path.to.my.handler'] # If you need custom exception handlers
7878
```
7979

80-
- Include `django_rq.urls` in your `urls.py`:
80+
## Admin Integration
81+
82+
_Changed in Version 3.0_
83+
Django-RQ automatically integrates with Django's admin interface. Once installed, navigate to `/admin/django_rq/dashboard/` to access:
84+
- Queue statistics and monitoring dashboard
85+
- Job registry browsers (scheduled, started, finished, failed, deferred)
86+
- Worker management
87+
- Prometheus metrics endpoint (if `prometheus_client` is installed)
88+
89+
The views are automatically registered in Django admin and a link to the dashboard is added to the admin interface's sidebar. If you want to disable this link, add `RQ_SHOW_ADMIN_LINK = False` in `settings.py`.
90+
91+
### Standalone URLs (Alternative)
92+
93+
For advanced use cases, you can also include Django-RQ views at a custom URL prefix:
8194

8295
```python
96+
# urls.py
8397
urlpatterns += [
8498
path('django-rq/', include('django_rq.urls'))
8599
]
86100
```
87101

102+
This makes views accessible at `/django-rq/` instead of within the admin interface at `/admin/django_rq/dashboard/`.
103+
88104
## Usage
89105

90106
### Putting jobs in the queue
@@ -307,11 +323,9 @@ python manage.py rqresume
307323

308324
### Queue Statistics
309325

310-
`django_rq` also provides a dashboard to monitor the status of your queues at `/django-rq/` (or whatever URL you set in your `urls.py` during installation).
311-
312-
You can also add a link to this dashboard in `/admin` by adding `RQ_SHOW_ADMIN_LINK = True` in `settings.py`. Be careful though, this will override the default admin template so it may interfere with other apps that modify the default admin template.
326+
_Changed in Version 3.0_
313327

314-
These statistics are also available in JSON format via `/django-rq/stats.json`, which is accessible to staff members. If you need to access this view via other HTTP clients (for monitoring purposes), you can define `RQ_API_TOKEN`. Then, include the token in the Authorization header as a Bearer token: `Authorization: Bearer <token>` and access it via `/django-rq/stats.json`.
328+
Various queue statistics are also available in JSON format via `/django-rq/stats.json`, which is accessible via a bearer token authentication scheme (defined in `settings.py` as `RQ_API_TOKEN`). Then, include the token in the Authorization header as a Bearer token: `Authorization: Bearer <token>` and access it via `/django-rq/stats.json`.
315329

316330
![Django RQ JSON dashboard](demo-django-rq-json-dashboard.png)
317331

django_rq/admin.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import wraps
12
from typing import Any, Optional
23

34
from django.contrib import admin
@@ -34,6 +35,37 @@ def changelist_view(self, request: HttpRequest, extra_context: Optional[dict[str
3435
# proxy request to stats view
3536
return stats_views.stats(request)
3637

38+
def get_urls(self):
39+
"""
40+
Register Django-RQ views within Django admin.
41+
42+
URLs will be available at /admin/django_rq/dashboard/<pattern>/
43+
This provides automatic integration without requiring users to edit urls.py.
44+
45+
Uses two sets of URL patterns:
46+
- API patterns (stats_json, prometheus_metrics): NOT wrapped, support API token auth
47+
- Admin patterns (all other views): Wrapped with admin_view for session auth
48+
"""
49+
# Import inside method to avoid circular imports
50+
from .urls import get_admin_urlpatterns, get_api_urlpatterns
51+
52+
def wrap(view):
53+
"""Wrap view with admin_site.admin_view for permission checking"""
54+
55+
@wraps(view)
56+
def wrapper(*args, **kwargs):
57+
return self.admin_site.admin_view(view)(*args, **kwargs)
58+
59+
return wrapper
60+
61+
# Get both sets of URL patterns
62+
api_urls = get_api_urlpatterns() # Not wrapped - have their own auth
63+
admin_urls = get_admin_urlpatterns(view_wrapper=wrap) # Wrapped with admin auth
64+
65+
# Combine and add to standard ModelAdmin URLs
66+
return api_urls + admin_urls + super().get_urls()
67+
3768

69+
# Register the Dashboard model with admin if enabled.
3870
if settings.SHOW_ADMIN_LINK:
39-
admin.site.register(models.Queue, QueueAdmin)
71+
admin.site.register(models.Dashboard, QueueAdmin)

django_rq/models.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
got_request_exception.connect(thread_queue.clear)
1111

1212

13-
class Queue(models.Model):
14-
"""Placeholder model with no database table, but with django admin page
15-
and contenttype permission"""
13+
class Dashboard(models.Model):
14+
"""
15+
Admin-only model for Django-RQ dashboard integration.
16+
"""
1617

1718
class Meta:
18-
managed = False # not in Django's database
19+
managed = False # No database table - admin integration only
1920
default_permissions = ()
2021
permissions = [['view', 'Access admin page']]
22+
verbose_name = 'Django-RQ'
23+
verbose_name_plural = 'Django-RQ'

django_rq/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.conf import settings
55
from django.core.exceptions import ImproperlyConfigured
66

7-
SHOW_ADMIN_LINK = getattr(settings, 'RQ_SHOW_ADMIN_LINK', False)
7+
SHOW_ADMIN_LINK = getattr(settings, 'RQ_SHOW_ADMIN_LINK', True)
88

99
NAME = getattr(settings, 'RQ_NAME', 'default')
1010
BURST: bool = getattr(settings, 'RQ_BURST', False)

django_rq/urls.py

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,89 @@
1-
from django.urls import path, re_path
1+
from typing import Callable, Optional
2+
3+
from django.urls import URLPattern, path, re_path
24

35
from . import cron_views, stats_views, views
46
from .contrib.prometheus import RQCollector
57

6-
metrics_view = (
7-
[
8-
re_path(r'^metrics/?$', stats_views.prometheus_metrics, name='rq_metrics'),
8+
9+
def get_api_urlpatterns() -> list[URLPattern]:
10+
"""
11+
Get URL patterns for views that have their own authentication (API tokens).
12+
13+
These views should NOT be wrapped with admin_view because they support
14+
API token authentication that must work without Django session auth.
15+
16+
Returns:
17+
List of URL patterns for API-authenticated views.
18+
"""
19+
# Conditional metrics view (only if prometheus_client is installed)
20+
metrics_view = (
21+
[
22+
re_path(r'^metrics/?$', stats_views.prometheus_metrics, name='rq_metrics'),
23+
]
24+
if RQCollector is not None
25+
else []
26+
)
27+
28+
return [
29+
# Stats JSON (supports API token authentication)
30+
re_path(r'^stats.json/?$', stats_views.stats_json, name='rq_home_json'),
31+
re_path(r'^stats.json/(?P<token>[\w]+)?/?$', stats_views.stats_json, name='rq_home_json'),
32+
# Prometheus metrics (supports API token authentication)
33+
*metrics_view,
34+
]
35+
36+
37+
def get_admin_urlpatterns(view_wrapper: Optional[Callable] = None) -> list[URLPattern]:
38+
"""
39+
Get URL patterns for views that should be wrapped with admin authentication.
40+
41+
Args:
42+
view_wrapper: Optional function to wrap each view (e.g., admin_site.admin_view).
43+
44+
Returns:
45+
List of URL patterns for admin-authenticated views.
46+
"""
47+
48+
def maybe_wrap(view: Callable) -> Callable:
49+
"""Apply wrapper if provided, otherwise return view as-is"""
50+
return view_wrapper(view) if view_wrapper else view
51+
52+
return [
53+
# Dashboard
54+
path('', maybe_wrap(stats_views.stats), name='rq_home'),
55+
# Queue views
56+
path('queues/<int:queue_index>/', maybe_wrap(views.jobs), name='rq_jobs'),
57+
path('queues/<int:queue_index>/finished/', maybe_wrap(views.finished_jobs), name='rq_finished_jobs'),
58+
path('queues/<int:queue_index>/failed/', maybe_wrap(views.failed_jobs), name='rq_failed_jobs'),
59+
path('queues/<int:queue_index>/failed/clear/', maybe_wrap(views.delete_failed_jobs), name='rq_delete_failed_jobs'),
60+
path('queues/<int:queue_index>/scheduled/', maybe_wrap(views.scheduled_jobs), name='rq_scheduled_jobs'),
61+
path('queues/<int:queue_index>/started/', maybe_wrap(views.started_jobs), name='rq_started_jobs'),
62+
path('queues/<int:queue_index>/deferred/', maybe_wrap(views.deferred_jobs), name='rq_deferred_jobs'),
63+
path('queues/<int:queue_index>/empty/', maybe_wrap(views.clear_queue), name='rq_clear'),
64+
path('queues/<int:queue_index>/requeue-all/', maybe_wrap(views.requeue_all), name='rq_requeue_all'),
65+
# Job detail and actions
66+
path('queues/<int:queue_index>/<str:job_id>/', maybe_wrap(views.job_detail), name='rq_job_detail'),
67+
path('queues/<int:queue_index>/<str:job_id>/delete/', maybe_wrap(views.delete_job), name='rq_delete_job'),
68+
path('queues/<int:queue_index>/<str:job_id>/requeue/', maybe_wrap(views.requeue_job_view), name='rq_requeue_job'),
69+
path('queues/<int:queue_index>/<str:job_id>/enqueue/', maybe_wrap(views.enqueue_job), name='rq_enqueue_job'),
70+
path('queues/<int:queue_index>/<str:job_id>/stop/', maybe_wrap(views.stop_job), name='rq_stop_job'),
71+
# Bulk actions
72+
path('queues/confirm-action/<int:queue_index>/', maybe_wrap(views.confirm_action), name='rq_confirm_action'),
73+
path('queues/actions/<int:queue_index>/', maybe_wrap(views.actions), name='rq_actions'),
74+
# Workers
75+
path('workers/<int:queue_index>/', maybe_wrap(views.workers), name='rq_workers'),
76+
path('workers/<int:queue_index>/<str:key>/', maybe_wrap(views.worker_details), name='rq_worker_details'),
77+
# Schedulers
78+
path('schedulers/<int:scheduler_index>/', maybe_wrap(views.scheduler_jobs), name='rq_scheduler_jobs'),
79+
path(
80+
'cron-schedulers/<int:connection_index>/<str:scheduler_name>/',
81+
maybe_wrap(cron_views.cron_scheduler_detail),
82+
name='rq_cron_scheduler_detail',
83+
),
984
]
10-
if RQCollector is not None
11-
else []
12-
)
13-
14-
urlpatterns = [
15-
path('', stats_views.stats, name='rq_home'),
16-
re_path(r'^stats.json/?$', stats_views.stats_json, name='rq_home_json'),
17-
re_path(r'^stats.json/(?P<token>[\w]+)?/?$', stats_views.stats_json, name='rq_home_json'),
18-
*metrics_view,
19-
path('queues/<int:queue_index>/', views.jobs, name='rq_jobs'),
20-
path('workers/<int:queue_index>/', views.workers, name='rq_workers'),
21-
path('workers/<int:queue_index>/<str:key>/', views.worker_details, name='rq_worker_details'),
22-
path('queues/<int:queue_index>/finished/', views.finished_jobs, name='rq_finished_jobs'),
23-
path('queues/<int:queue_index>/failed/', views.failed_jobs, name='rq_failed_jobs'),
24-
path('queues/<int:queue_index>/failed/clear/', views.delete_failed_jobs, name='rq_delete_failed_jobs'),
25-
path('queues/<int:queue_index>/scheduled/', views.scheduled_jobs, name='rq_scheduled_jobs'),
26-
path('queues/<int:queue_index>/started/', views.started_jobs, name='rq_started_jobs'),
27-
path('queues/<int:queue_index>/deferred/', views.deferred_jobs, name='rq_deferred_jobs'),
28-
path('queues/<int:queue_index>/empty/', views.clear_queue, name='rq_clear'),
29-
path('queues/<int:queue_index>/requeue-all/', views.requeue_all, name='rq_requeue_all'),
30-
path('queues/<int:queue_index>/<str:job_id>/', views.job_detail, name='rq_job_detail'),
31-
path('queues/<int:queue_index>/<str:job_id>/delete/', views.delete_job, name='rq_delete_job'),
32-
path('queues/confirm-action/<int:queue_index>/', views.confirm_action, name='rq_confirm_action'),
33-
path('queues/actions/<int:queue_index>/', views.actions, name='rq_actions'),
34-
path('queues/<int:queue_index>/<str:job_id>/requeue/', views.requeue_job_view, name='rq_requeue_job'),
35-
path('queues/<int:queue_index>/<str:job_id>/enqueue/', views.enqueue_job, name='rq_enqueue_job'),
36-
path('queues/<int:queue_index>/<str:job_id>/stop/', views.stop_job, name='rq_stop_job'),
37-
path('schedulers/<int:scheduler_index>/', views.scheduler_jobs, name='rq_scheduler_jobs'),
38-
path(
39-
'cron-schedulers/<int:connection_index>/<str:scheduler_name>/',
40-
cron_views.cron_scheduler_detail,
41-
name='rq_cron_scheduler_detail',
42-
),
43-
]
85+
86+
87+
# Standalone URL patterns (for use with include('django_rq.urls'))
88+
# Combines both API and admin patterns without wrapping
89+
urlpatterns = get_api_urlpatterns() + get_admin_urlpatterns()

0 commit comments

Comments
 (0)