From 16efb32b8871d2b0a0001758a216ed60a2695372 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Wed, 22 Oct 2025 15:54:31 +0100 Subject: [PATCH 1/5] setup preliminary tasks panel logic --- debug_toolbar/panels/tasks.py | 15 +++++++++++++++ debug_toolbar/settings.py | 1 + .../templates/debug_toolbar/panels/tasks.html | 14 ++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 debug_toolbar/panels/tasks.py create mode 100644 debug_toolbar/templates/debug_toolbar/panels/tasks.html diff --git a/debug_toolbar/panels/tasks.py b/debug_toolbar/panels/tasks.py new file mode 100644 index 000000000..36fc9f534 --- /dev/null +++ b/debug_toolbar/panels/tasks.py @@ -0,0 +1,15 @@ +from django.utils.translation import gettext_lazy as _ + +from debug_toolbar.panels import Panel + + +class TasksPanel(Panel): + """ + Panel that displays Django tasks queued or executed during the + processing of the request. + """ + + title = _("Tasks") + template = "debug_toolbar/panels/tasks.html" + + is_async = True diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index ba64c8273..2dbc58a31 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -82,6 +82,7 @@ def get_config(): "debug_toolbar.panels.cache.CachePanel", "debug_toolbar.panels.signals.SignalsPanel", "debug_toolbar.panels.community.CommunityPanel", + "debug_toolbar.panels.tasks.TasksPanel", "debug_toolbar.panels.redirects.RedirectsPanel", "debug_toolbar.panels.profiling.ProfilingPanel", ] diff --git a/debug_toolbar/templates/debug_toolbar/panels/tasks.html b/debug_toolbar/templates/debug_toolbar/panels/tasks.html new file mode 100644 index 000000000..b37495c40 --- /dev/null +++ b/debug_toolbar/templates/debug_toolbar/panels/tasks.html @@ -0,0 +1,14 @@ + + + + + + + + {% for task in tasks %} + + + + {% endfor %} + +
Task Info
{{ task }}
From d4b08edb4f383c77dd650266ce5780feb2416867 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Wed, 22 Oct 2025 19:28:18 +0100 Subject: [PATCH 2/5] Set dummy TasksPanel methods --- debug_toolbar/panels/tasks.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/debug_toolbar/panels/tasks.py b/debug_toolbar/panels/tasks.py index 36fc9f534..cd2954f4c 100644 --- a/debug_toolbar/panels/tasks.py +++ b/debug_toolbar/panels/tasks.py @@ -1,4 +1,4 @@ -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_lazy as _, ngettext from debug_toolbar.panels import Panel @@ -13,3 +13,27 @@ class TasksPanel(Panel): template = "debug_toolbar/panels/tasks.html" is_async = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.queued_tasks = [] + + @property + def nav_subtitle(self): + num_tasks = self.get_stats()["total_tasks"] + return ngettext( + "%(num_tasks)d task enqueued", + "%(num_tasks)d tasks enqueued", + num_tasks, + ) % {"num_tasks": num_tasks} + + def generate_stats(self, request, response): + stats = {"tasks": self.queued_tasks, "total_tasks": len(self.queued_tasks)} + + self.record_stats(stats) + + def enable_instrumentation(self): + pass + + def disable_instrumentation(self): + pass From afa229764185e326f384daadbf529e48e7849ce7 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Wed, 29 Oct 2025 19:06:12 +0100 Subject: [PATCH 3/5] Experiment with tasks panel --- debug_toolbar/panels/tasks.py | 52 +++++++++++++++++++++++++++++++++-- example/async_/tasks.py | 19 +++++++++++++ example/views.py | 8 ++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 example/async_/tasks.py diff --git a/debug_toolbar/panels/tasks.py b/debug_toolbar/panels/tasks.py index cd2954f4c..e81449fcd 100644 --- a/debug_toolbar/panels/tasks.py +++ b/debug_toolbar/panels/tasks.py @@ -33,7 +33,55 @@ def generate_stats(self, request, response): self.record_stats(stats) def enable_instrumentation(self): - pass + """Hook into task system to collect queued tasks""" + try: + import django + + if django.VERSION < (6, 0): + return + from django.tasks import Task + + print("[TasksPanel] instrumentation enabled:", hasattr(Task, "enqueue")) + + # Store original enqueue method + if hasattr(Task, "enqueue"): + self._original_enqueue = Task.enqueue + + def wrapped_enqueue(task, *args, **kwargs): + result = self._original_enqueue(task, *args, **kwargs).return_value + self._record_task(task, args, kwargs, result) + return result + + Task.enqueue = wrapped_enqueue + except (ImportError, AttributeError): + pass + + def _record_task(self, task, args, kwargs, result): + """Record a task that was queued""" + task_info = { + "name": getattr(task, "__name__", str(task)), + "args": repr(args) if args else "", + "kwargs": repr(kwargs) if kwargs else "", + } + self.queued_tasks.append(task_info) def disable_instrumentation(self): - pass + """Restore original methods""" + try: + from django.tasks import Task + + if hasattr(self, "_original_enqueue"): + Task.enqueue = self._original_enqueue + except (ImportError, AttributeError): + pass + + def _check_tasks_available(self): + """Check if Django tasks system is available""" + try: + import django + + if django.VERSION < (6, 0): + return False + return True + except (ImportError, AttributeError): + return False diff --git a/example/async_/tasks.py b/example/async_/tasks.py new file mode 100644 index 000000000..24dec438c --- /dev/null +++ b/example/async_/tasks.py @@ -0,0 +1,19 @@ +try: + from django.tasks import task +except ImportError: + # Define a fallback decorator + def task(func=None, **kwargs): + def decorator(f): + return f + + return decorator if func is None else decorator(func) + + +@task +def send_welcome_message(message): + return f"Sent message: {message}" + + +@task +def generate_report(report_id): + return f"Report {report_id} generated" diff --git a/example/views.py b/example/views.py index b87b02661..60b3ce6b4 100644 --- a/example/views.py +++ b/example/views.py @@ -1,5 +1,6 @@ import asyncio +import django from asgiref.sync import sync_to_async from django.contrib.auth.models import User from django.core.cache import cache @@ -21,6 +22,13 @@ def jinja2_view(request): async def async_home(request): + if django.VERSION >= (6, 0): + from .async_.tasks import generate_report, send_welcome_message + + # Queue some tasks + send_welcome_message.enqueue(message="hi there") + generate_report.enqueue(report_id=456) + return await sync_to_async(render)(request, "index.html") From 6a1a90cc86a45424a4183759a5c3c1b59ce19957 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Wed, 26 Nov 2025 11:33:46 +0100 Subject: [PATCH 4/5] Use tasks support checker in instrumentation method --- debug_toolbar/panels/tasks.py | 43 ++++++++----------- .../templates/debug_toolbar/panels/tasks.html | 4 +- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/debug_toolbar/panels/tasks.py b/debug_toolbar/panels/tasks.py index e81449fcd..a9b41f89c 100644 --- a/debug_toolbar/panels/tasks.py +++ b/debug_toolbar/panels/tasks.py @@ -1,3 +1,4 @@ +import django from django.utils.translation import gettext_lazy as _, ngettext from debug_toolbar.panels import Panel @@ -34,27 +35,23 @@ def generate_stats(self, request, response): def enable_instrumentation(self): """Hook into task system to collect queued tasks""" - try: - import django - - if django.VERSION < (6, 0): - return - from django.tasks import Task + if self._tasks_available is False: + # Django tasks not available means that we cannot instrument + return + from django.tasks import Task - print("[TasksPanel] instrumentation enabled:", hasattr(Task, "enqueue")) + print("[TasksPanel] instrumentation enabled:", hasattr(Task, "enqueue")) - # Store original enqueue method - if hasattr(Task, "enqueue"): - self._original_enqueue = Task.enqueue + # Store original enqueue method + if hasattr(Task, "enqueue"): + self._original_enqueue = Task.enqueue - def wrapped_enqueue(task, *args, **kwargs): - result = self._original_enqueue(task, *args, **kwargs).return_value - self._record_task(task, args, kwargs, result) - return result + def wrapped_enqueue(task, *args, **kwargs): + result = self._original_enqueue(task, *args, **kwargs).return_value + self._record_task(task, args, kwargs, result) + return result - Task.enqueue = wrapped_enqueue - except (ImportError, AttributeError): - pass + Task.enqueue = wrapped_enqueue def _record_task(self, task, args, kwargs, result): """Record a task that was queued""" @@ -75,13 +72,9 @@ def disable_instrumentation(self): except (ImportError, AttributeError): pass - def _check_tasks_available(self): + @property + def _tasks_available(self) -> bool: """Check if Django tasks system is available""" - try: - import django - - if django.VERSION < (6, 0): - return False - return True - except (ImportError, AttributeError): + if django.VERSION < (6, 0): return False + return True diff --git a/debug_toolbar/templates/debug_toolbar/panels/tasks.html b/debug_toolbar/templates/debug_toolbar/panels/tasks.html index b37495c40..bb05406ad 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/tasks.html +++ b/debug_toolbar/templates/debug_toolbar/panels/tasks.html @@ -1,7 +1,9 @@ +{% load i18n %} + - + From 5e3c313edea00662859f5a98411e3f0c424a2932 Mon Sep 17 00:00:00 2001 From: Chiemezuo Date: Wed, 26 Nov 2025 23:33:45 +0100 Subject: [PATCH 5/5] Add TasksPanel to PANEL_KEYS in tests --- tests/panels/test_history.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/panels/test_history.py b/tests/panels/test_history.py index 980ec4a67..aff26636d 100644 --- a/tests/panels/test_history.py +++ b/tests/panels/test_history.py @@ -41,7 +41,8 @@ def test_post_json(self): "/", data=data, content_type="application/json", - CONTENT_TYPE="application/json", # Force django test client to add the content-type even if no data + # Force django test client to add the content-type even if no data + CONTENT_TYPE="application/json", ) response = self.panel.process_request(self.request) self.panel.generate_stats(self.request, response) @@ -82,6 +83,7 @@ class HistoryViewsTestCase(IntegrationTestCase): "CachePanel", "SignalsPanel", "CommunityPanel", + "TasksPanel", "ProfilingPanel", }
Task Info{% translate "Task Info" %}