Skip to content

Commit 1acf00f

Browse files
authored
cron_scheduler_details.html now shows crob job details (#766)
* cron_scheduler_details.html now shows crob job details * Add compatibility with redis-py 7.2
1 parent a46a646 commit 1acf00f

File tree

5 files changed

+145
-1
lines changed

5 files changed

+145
-1
lines changed

django_rq/cron.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,44 @@ def connection_index(self) -> int:
170170
# This should never happen - if we have a connection_config, it must be in the list
171171
raise ValueError('Could not find matching connection config in unique configs.')
172172

173+
def get_jobs_data(self) -> list[dict[str, Any]]:
174+
"""Returns a list of dicts describing each registered cron job for display purposes."""
175+
cron_jobs_data: list[dict[str, object]] = []
176+
177+
for job in self.get_jobs():
178+
cron_string = job.cron
179+
interval = job.interval
180+
181+
if cron_string:
182+
schedule = f"cron: {cron_string}"
183+
elif interval is not None:
184+
schedule = f"every {interval} seconds"
185+
else:
186+
schedule = "-"
187+
188+
job_options = job.job_options
189+
options: list[str] = []
190+
for key in ('job_timeout', 'result_ttl', 'ttl', 'failure_ttl'):
191+
value = job_options.get(key)
192+
if value is not None:
193+
options.append(f"{key}={value}")
194+
195+
cron_jobs_data.append(
196+
{
197+
"func_name": job.func_name,
198+
"queue_name": job.queue_name,
199+
"schedule": schedule,
200+
"latest_enqueue_time": job.latest_enqueue_time,
201+
"next_enqueue_time": job.next_enqueue_time,
202+
"args": job.args,
203+
"kwargs": job.kwargs,
204+
"meta": job_options.get('meta'),
205+
"options": options,
206+
}
207+
)
208+
209+
return cron_jobs_data
210+
173211
@classmethod
174212
def all(cls, connection: Redis, cleanup: bool = True) -> list['DjangoCronScheduler']: # type: ignore[override]
175213
"""

django_rq/cron_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def cron_scheduler_detail(request, connection_index: int, scheduler_name: str):
3939
context_data = {
4040
**each_context(request),
4141
"scheduler": scheduler,
42+
"cron_jobs": scheduler.get_jobs_data(),
4243
}
4344

4445
return render(request, 'django_rq/cron_scheduler_detail.html', context_data)

django_rq/templates/django_rq/cron_scheduler_detail.html

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{% block extrastyle %}
66
{{ block.super }}
77
<link rel="stylesheet" href="{% static 'admin/css/forms.css' %}">
8+
<link rel="stylesheet" href="{% static 'admin/css/changelists.css' %}">
89
{% endblock %}
910

1011
{% block title %}Cron Scheduler: {{ scheduler.name }}{% endblock %}
@@ -94,5 +95,63 @@
9495

9596
</fieldset>
9697

98+
<div class="module" id="changelist">
99+
<h2>Cron Jobs</h2>
100+
<div class="results">
101+
{% if cron_jobs %}
102+
<table id="result_list">
103+
<thead>
104+
<tr>
105+
<th><div class="text"><span>Callable</span></div></th>
106+
<th><div class="text"><span>Queue</span></div></th>
107+
<th><div class="text"><span>Schedule</span></div></th>
108+
<th><div class="text"><span>Next Enqueue</span></div></th>
109+
<th><div class="text"><span>Latest Enqueue</span></div></th>
110+
<th><div class="text"><span>Args</span></div></th>
111+
<th><div class="text"><span>Kwargs</span></div></th>
112+
<th><div class="text"><span>Meta</span></div></th>
113+
<th><div class="text"><span>Options</span></div></th>
114+
</tr>
115+
</thead>
116+
<tbody>
117+
{% for cron_job in cron_jobs %}
118+
<tr class="{% cycle 'row1' 'row2' %}">
119+
<td>{{ cron_job.func_name|default:"-" }}</td>
120+
<td>{{ cron_job.queue_name|default:"-" }}</td>
121+
<td>{{ cron_job.schedule }}</td>
122+
<td>
123+
{% if cron_job.next_enqueue_time %}
124+
{{ cron_job.next_enqueue_time|to_localtime|date:"Y-m-d, H:i:s" }}
125+
{% else %}
126+
-
127+
{% endif %}
128+
</td>
129+
<td>
130+
{% if cron_job.latest_enqueue_time %}
131+
{{ cron_job.latest_enqueue_time|to_localtime|date:"Y-m-d, H:i:s" }}
132+
{% else %}
133+
-
134+
{% endif %}
135+
</td>
136+
<td>{{ cron_job.args|default_if_none:"-" }}</td>
137+
<td>{{ cron_job.kwargs|default_if_none:"-" }}</td>
138+
<td>{{ cron_job.meta|default_if_none:"-" }}</td>
139+
<td>
140+
{% for option in cron_job.options %}
141+
{{ option }}{% if not forloop.last %}<br>{% endif %}
142+
{% empty %}
143+
-
144+
{% endfor %}
145+
</td>
146+
</tr>
147+
{% endfor %}
148+
</tbody>
149+
</table>
150+
{% else %}
151+
<p class="paginator">No persisted cron jobs found</p>
152+
{% endif %}
153+
</div>
154+
</div>
155+
97156
</div>
98157
{% endblock %}

django_rq/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def get_statistics(run_maintenance_tasks: bool = False) -> dict[str, list[dict[s
7575
connection_kwargs.pop('parser_class', None)
7676
connection_kwargs.pop('retry', None)
7777
connection_kwargs.pop('password', None)
78+
connection_kwargs.pop('driver_info', None)
7879

7980
queue_data = {
8081
'name': queue.name,

tests/test_cron.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.urls import reverse
1010
from rq.cron import CronJob
1111

12+
from django_rq import get_connection
1213
from django_rq.cron import DjangoCronScheduler
1314
from tests.fixtures import say_hello
1415

@@ -180,7 +181,19 @@ def test_cron_scheduler_detail_view(self):
180181
"""Test cron scheduler detail view with various scenarios."""
181182
# Create a real scheduler and register it
182183
scheduler = DjangoCronScheduler(name='test-scheduler')
183-
scheduler.register(say_hello, "default", interval=60)
184+
scheduler.register(
185+
say_hello,
186+
"default",
187+
interval=60,
188+
args=("hello",),
189+
kwargs={"target": "world"},
190+
meta={"source": "test"},
191+
job_timeout=20,
192+
result_ttl=500,
193+
ttl=60,
194+
failure_ttl=120,
195+
)
196+
scheduler.register(say_hello, "default", cron="*/5 * * * *")
184197
scheduler.register_birth()
185198

186199
for prefix in ('admin:django_rq_', 'django_rq:'):
@@ -190,7 +203,24 @@ def test_cron_scheduler_detail_view(self):
190203
response = self.client.get(url)
191204
self.assertEqual(response.status_code, 200)
192205
self.assertEqual(response.context['scheduler'].name, 'test-scheduler')
206+
self.assertEqual(len(response.context['cron_jobs']), 2)
207+
first_cron_job = response.context['cron_jobs'][0]
208+
self.assertEqual(first_cron_job['func_name'], 'tests.fixtures.say_hello')
209+
self.assertEqual(first_cron_job['queue_name'], 'default')
210+
self.assertEqual(first_cron_job['schedule'], 'every 60 seconds')
193211
self.assertContains(response, 'test-scheduler')
212+
self.assertContains(response, 'Cron Jobs')
213+
self.assertContains(response, 'tests.fixtures.say_hello')
214+
self.assertContains(response, 'default')
215+
self.assertContains(response, 'every 60 seconds')
216+
self.assertContains(response, 'cron: */5 * * * *')
217+
self.assertContains(response, 'hello')
218+
self.assertContains(response, 'world')
219+
self.assertContains(response, 'source')
220+
self.assertContains(response, 'job_timeout=20')
221+
self.assertContains(response, 'result_ttl=500')
222+
self.assertContains(response, 'ttl=60')
223+
self.assertContains(response, 'failure_ttl=120')
194224

195225
# Test 2: Non-existent scheduler returns 404
196226
url = reverse(f'{prefix}cron_scheduler_detail', args=[connection_index, 'nonexistent-scheduler'])
@@ -204,3 +234,18 @@ def test_cron_scheduler_detail_view(self):
204234

205235
# Clean up
206236
scheduler.register_death()
237+
238+
def test_cron_scheduler_detail_view_with_no_jobs(self):
239+
"""Test cron scheduler detail view gracefully handles schedulers with no jobs."""
240+
scheduler = DjangoCronScheduler(connection=get_connection("default"), name='empty-scheduler')
241+
scheduler.register_birth()
242+
243+
for prefix in ('admin:django_rq_', 'django_rq:'):
244+
url = reverse(f'{prefix}cron_scheduler_detail', args=[scheduler.connection_index, 'empty-scheduler'])
245+
response = self.client.get(url)
246+
self.assertEqual(response.status_code, 200)
247+
self.assertEqual(response.context['scheduler'].name, 'empty-scheduler')
248+
self.assertEqual(response.context['cron_jobs'], [])
249+
self.assertContains(response, 'No persisted cron jobs found')
250+
251+
scheduler.register_death()

0 commit comments

Comments
 (0)