Skip to content

Add Tasks panel showing tasks queued during the request#2407

Open
onxxdatas wants to merge 9 commits into
django-commons:mainfrom
onxxdatas:tasks-panel
Open

Add Tasks panel showing tasks queued during the request#2407
onxxdatas wants to merge 9 commits into
django-commons:mainfrom
onxxdatas:tasks-panel

Conversation

@onxxdatas

@onxxdatas onxxdatas commented Jul 9, 2026

Copy link
Copy Markdown

Description

Adds a new panel using Django's built-in tasks framework (django.tasks, added in Django 6.0). Shows task name, queue, backend, priority, run_after, status, and arguments for each task queued during the request. On Django < 6.0, the panel explains that upgrading is required instead of showing task data..

Fixes #2204

Checklist:

  • I have added the relevant tests for this change.
  • I have added an item to the Pending section of docs/changes.rst.

AI/LLM Usage

  • This PR includes code generated with the help of an AI/LLM

Adds a new panel using Django's built-in tasks framework (django.tasks, added in Django 6.0). Shows task name, queue, backend, priority, run_after, status, and arguments for each task queued during the request. On Django < 6.0, the panel explains that upgrading is required instead of showing task data.

Closes django-commons#2204
@onxxdatas onxxdatas mentioned this pull request Jul 9, 2026
@matthiask

matthiask commented Jul 9, 2026

Copy link
Copy Markdown
Member

Hi @onxxdatas

Thanks for your contribution! Can you post a screenshot of how the panel looks when some tasks have been enqueued? That would be very helpful.

@matthiask

Copy link
Copy Markdown
Member

And also, did you see #2235 ? How does this change compare to the panel proposed in the other PR?

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  debug_toolbar
  _compat.py 36
  debug_toolbar/panels
  tasks.py
Project Total  

This report was generated by python-coverage-comment-action

@onxxdatas

Copy link
Copy Markdown
Author

@matthiask Here's the panel with tasks enqueued. I also added a small demo page
to the example project (/tasks/) for testing this going forward

Enqueued tasks:
image

Tasks history:
image

@onxxdatas

Copy link
Copy Markdown
Author

And also, did you see #2235 ? How does this change compare to the panel proposed in the other PR?

I saw two structural differences in #2235 compared to mine. First of all it instruments by monkeypatching Task.enqueue directly on the class (Task.enqueue = wrapped_enqueue). Since the panel is per request and is_async = True, this isn't safe under concurrent&async requests. Two overlapping requests can race on the patch, with disable_instrumentation() potentially restoring the wrong 'original' and leaving tasks misattributed or the method unpatched afterward.

Second one is that on django < 6.0, _tasks_available returns False and instrumentation just no ops silently so the panel still shows up but always displays zero tasks with no indication to the user that it's unsupported on their Django version.

@tim-schilling tim-schilling left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a great start! I have a handful of small requests. I'd be happy to have this merged with those addressed (or resolving the conversations through discussion if you disagree).

One thing we probably do want to include eventually is the stacktrace from where the task was queued, the same way we can determine where a cache call or SQL query was ran. I would suggest looking at the CachePanel for inspiration as it should be simpler. If you want to add that in a follow-up PR that's fine too.

Comment thread debug_toolbar/panels/tasks.py Outdated
def nav_subtitle(self):
if task_enqueued is None:
return _("Requires Django 6.0+")
tasks = self.get_stats().get("tasks", [])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tasks = self.get_stats().get("tasks", [])
tasks = self.get_stats()["tasks"]

Comment thread debug_toolbar/panels/tasks.py Outdated
"run_after": task.run_after,
"takes_context": task.takes_context,
"args": task_result.args,
"kwargs": task_result.kwargs,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use toolbar.utils.sanitize_and_sort_request_vars here?

Comment thread example/views.py Outdated
task = None


if task is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like comparing on the Django version will be easier for us to keep up dated as versions pass rather than checking the imported variable. It may be better to just do one big if statement for tasks_view and example_task as well then.

Comment thread tests/panels/test_tasks.py Outdated
Comment on lines +8 to +11
try:
from django.tasks import task
except ImportError:
task = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's extract this into a compat.py file and when it doesn't exist, create a pass-through decorator. That way we can can use skipUnless with the version which is more typical.

Comment thread tests/panels/test_tasks.py Outdated
Comment on lines +51 to +59
self.assertTrue(stats["tasks_available"])
self.assertEqual(len(stats["tasks"]), 1)

recorded = stats["tasks"][0]
self.assertEqual(recorded["name"], f"{__name__}.sample_task")
self.assertEqual(list(recorded["args"]), [2])
self.assertEqual(dict(recorded["kwargs"]), {"y": 3})
self.assertEqual(recorded["queue_name"], "default")
self.assertEqual(recorded["backend"], "default")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think we may want to compare the stats dictionary as a whole. For example self.assertEqual(stats, { ... }) instead of the individual assertions.

Comment thread tests/panels/test_tasks.py Outdated
On Django < 6.0, django.tasks doesn't exist, so the panel should
explain that instead of showing task data.
"""
with mock.patch("debug_toolbar.panels.tasks.task_enqueued", None):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than patching this, we could do @skipUnless(django.VERSION < (6, 0))

@onxxdatas

Copy link
Copy Markdown
Author

@tim-schilling Thanks for the thorough review👍🏼 I've pushed a commit addressing all 6 points.

@onxxdatas

Copy link
Copy Markdown
Author

@tim-schilling The failures only happen on the djmain test matrix, on Python 3.13/3.14 & dj52 and dj60 (the two released Django versions) both pass. The cause is django-csp's CSPMiddleware, which still imports MiddlewareMixin from this deprecated location: MiddlewareMixin from django.utils.deprecation. Django's dev branch just started emitting that warning, and since the test runner treats warnings as errors (-b flag), it fails 4 CSP tests.

@codingjoe codingjoe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I am looking forward to this one 👍


# Implement the Panel API

nav_title = _("Tasks")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we cap, single words? I'd usually like to use capfirst or title in templates and keep translation placeholders ambiguous.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the translation string to _("tasks"). I left the template unchanged since that would affect all panel titles.

Comment thread debug_toolbar/panels/tasks.py Outdated
Comment on lines +37 to +38
if task_enqueued is None:
return _("Requires Django 6.0+")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting solution. A django.__VERSION__ condition might be easier to find when we remove Django 5.2 support.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, using django.VERSION is preferrable since those blocks can automatically be dropped using django-upgrade later.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to use django.VERSION for the compatibility check

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if task_enqueued is None:
return _("Requires Django 6.0+")
if django.VERSION < (6, 0):
return _("Requires Django 6.0+")

@onxxdatas this is what they are suggesting.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah-yeah, I just pushed all my changes

Comment on lines +49 to +60
{
"id": task_result.id,
"name": task.module_path,
"queue_name": task.queue_name,
"priority": task.priority,
"backend": task_result.backend,
"run_after": task.run_after,
"takes_context": task.takes_context,
"args": sanitize_and_sort_request_vars(task_result.args),
"kwargs": sanitize_and_sort_request_vars(task_result.kwargs),
"status": task_result.status,
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to come up with our custom data structure here? TaskResult holds all the important information.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can also fairly easily be JSON serialized, if that is needed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the custom dictionary and now we rely on TaskResult directly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codingjoe are you suggesting we shouldn't be appending a custom dictionary to tasks here?

@codingjoe codingjoe Jul 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was suggesting to just pass the TaskResult instance to the template. I don't see why we need this mapping here. In other words:

self.task: lists[TaskResult]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have any other change request based on my last commit?

"name": task.module_path,
"queue_name": task.queue_name,
"priority": task.priority,
"backend": task_result.backend,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual backend class is probably less interesting. The alias is usually probably the imporant information.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my decision, I updated the template to show backend alias from Tasks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've had people want to know what the backend is for caches as well. Plus using this in different environments may mean using different backends (Immediate vs DB vs something else)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I focused on the alias because it identifies the configured task backend, but I agree the actual backend implementation can be valuable when debugging differences between environments. What do you think about showing both alias and backend class at once?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just realizing they are one and the same. Poor naming on Django's site.

So task_result.backend is the alias.

<table>
<thead>
<tr>
<th>{% translate "Name" %}</th>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a general thought:

Suggested change
<th>{% translate "Name" %}</th>
<th>{% translate "name"|title %}</th>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@onxxdatas onxxdatas Jul 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, I lowercased the other translation strings too

Comment thread docs/changes.rst
Comment on lines +11 to +13
* Added a Tasks panel that shows tasks queued during the request via
Django's built-in tasks framework (``django.tasks``, Django 6.0+). On
older versions of Django, the panel explains that upgrading is required.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a merge error, but there seems to be duplication.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed!

Comment thread docs/panels.rst Outdated
Comment on lines +121 to +122
added in that release. On earlier versions of Django, the panel
explains that upgrading is required instead of showing task data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a little verbose. Most people will figure out that Django <6.0 doesn't have a task framework. The error message is good, but IMHO it doesn't need to be documented.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it just says "Requires Django 6.0+."

@matthiask

Copy link
Copy Markdown
Member

@tim-schilling The failures only happen on the djmain test matrix, on Python 3.13/3.14 & dj52 and dj60 (the two released Django versions) both pass. The cause is django-csp's CSPMiddleware, which still imports MiddlewareMixin from this deprecated location: MiddlewareMixin from django.utils.deprecation. Django's dev branch just started emitting that warning, and since the test runner treats warnings as errors (-b flag), it fails 4 CSP tests.

Yep, that's fixed now.

@onxxdatas
onxxdatas requested a review from tim-schilling July 15, 2026 16:10

@codingjoe codingjoe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost! Sorry, I found just a few more small things. All nit picks, but when in rome 🤷

<thead>
<tr>
<th>{% translate "name"|title %}</th>
<th>{% translate "queue"|title %}</th>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ambiguous, could be verb to queue

Suggested change
<th>{% translate "queue"|title %}</th>
<th>{% translate "queue"|title context "noun" %}</th>

<table>
<thead>
<tr>
<th>{% translate "name"|title %}</th>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ambiguous, could be verb to name

Suggested change
<th>{% translate "name"|title %}</th>
<th>{% translate "name"|title context "noun" %}</th>

@@ -0,0 +1,40 @@
{% load i18n %}
{% if not tasks_available %}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to avoid negations. You can do if task .. elif task_available .. else.

That being said, the task_available could be replaced with a Django version check too. Again, easier to remove once Django 5.2 goes the way of the dodo.

Comment thread debug_toolbar/panels/tasks.py Outdated
super().__init__(*args, **kwargs)
self.tasks = []

# Implement the Panel API

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it doesn't spark joy, it can go. — Marie Kondo

Suggested change
# Implement the Panel API

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. Didn't spark joy😄
Also fixed the "name"/"queue" ambiguity and the negation in the template, all pushed.

…able / else)

- Disambiguate 'name'/'queue' translations with context 'noun'
- Use django.VERSION instead of task_enqueued is None
- Drop the 'Implement the Panel API' comment
- Keep task stats as a dict mirroring TaskResult's own field names
  (raw TaskResult isn't JSON-serializable - Task.func breaks the
  history panel/async request storage round-trip)
- Revert nav_title/title to 'Tasks' (capitalized), matching every
  other panel, since panel_button.html has no capfirst/title filter
<table>
<thead>
<tr>
<th>{% translate "name"|title context "noun" %}</th>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name may be ambiguous here. Path or Python-path might be more fitting.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create Tasks panel

4 participants