diff --git a/scheduler/templates/admin/scheduler/job_detail.html b/scheduler/templates/admin/scheduler/job_detail.html
index 14faefa..af27f33 100644
--- a/scheduler/templates/admin/scheduler/job_detail.html
+++ b/scheduler/templates/admin/scheduler/job_detail.html
@@ -14,7 +14,7 @@
{% block content_title %}
Job {{ job.name }}
- {% if job.is_scheduled_task %}
+ {% if job.is_scheduled_task and job|scheduled_task %}
Link to scheduled job
diff --git a/scheduler/templatetags/scheduler_tags.py b/scheduler/templatetags/scheduler_tags.py
index 1ad26f2..dccad48 100644
--- a/scheduler/templatetags/scheduler_tags.py
+++ b/scheduler/templatetags/scheduler_tags.py
@@ -30,9 +30,12 @@ def get_item(dictionary: Dict, key):
@register.filter
-def scheduled_task(job: JobModel) -> Task:
- django_scheduled_task = get_scheduled_task(*job.args)
- return django_scheduled_task.get_absolute_url()
+def scheduled_task(job: JobModel) -> Optional[Task]:
+ try:
+ django_scheduled_task = get_scheduled_task(*job.args)
+ return django_scheduled_task.get_absolute_url()
+ except ValueError:
+ return None
@register.filter
diff --git a/scheduler/tests/test_views/test_queue_registry_jobs.py b/scheduler/tests/test_views/test_queue_registry_jobs.py
index dcb1cf0..64be4b0 100644
--- a/scheduler/tests/test_views/test_queue_registry_jobs.py
+++ b/scheduler/tests/test_views/test_queue_registry_jobs.py
@@ -6,6 +6,8 @@
from scheduler.helpers.queues import get_queue
from scheduler.tests.jobs import test_job
from scheduler.tests.test_views.base import BaseTestCase
+from scheduler.tests.testtools import task_factory
+from scheduler.models import TaskType, Task
class QueueRegistryJobsViewTest(BaseTestCase):
@@ -90,3 +92,51 @@ def test_started_jobs(self):
registry.add(queue.connection, job.name, time.time() + 20)
res = self.client.get(reverse("queue_registry_jobs", args=[queue_name, "active"]))
self.assertEqual(res.context["jobs"], [job])
+
+ def test_missing_task_doesnt_crash_job_detail_page(self):
+ """
+ Ensure that when a Task gets deleted and its Job doesn't get cleaned, the
+ job detail page doesn't raise an exception.
+ """
+ queue_name = "django_tasks_scheduler_test"
+
+ # No jobs in the queue
+ res = self.client.get(reverse("queue_registry_jobs", args=[queue_name, "scheduled"]))
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context["jobs"], [])
+
+ task = task_factory(TaskType.ONCE, queue="django_tasks_scheduler_test")
+
+ res = self.client.get(reverse("queue_registry_jobs", args=[queue_name, "scheduled"]))
+ self.assertEqual(res.status_code, 200)
+ job = res.context["jobs"][0]
+ self.assertTrue(job)
+ self.assertTrue(job.is_scheduled_task)
+ self.assertEqual(job.scheduled_task_id, task.pk)
+
+ # Job detail page works
+ url = reverse("job_details", args=[job.name])
+ res = self.client.get(url)
+ self.assertEqual(200, res.status_code)
+ self.assertIn("job", res.context)
+ self.assertEqual(res.context["job"], job)
+ self.assertNotContains(res, "ValueError('Invalid task type OnceTaskType')")
+ self.assertContains(res, "Link to scheduled job")
+
+ # Delete all tasks in bulk, this doesn't trigger the signal
+ # that would delete the corresponding scheduled jobs.
+ Task.objects.all().delete()
+
+ # The job lingers around :(
+ res = self.client.get(reverse("queue_registry_jobs", args=[queue_name, "scheduled"]))
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(job in res.context["jobs"])
+
+ # Job detail doesn't raise a 500
+ url = reverse("job_details", args=[job.name])
+ res = self.client.get(url)
+ self.assertEqual(200, res.status_code)
+ self.assertIn("job", res.context)
+ self.assertEqual(res.context["job"], job)
+ self.assertContains(res, "ValueError('Invalid task type OnceTaskType')")
+ self.assertNotContains(res, "Link to scheduled job")