diff --git a/debug_toolbar/panels/tasks.py b/debug_toolbar/panels/tasks.py new file mode 100644 index 000000000..a9b41f89c --- /dev/null +++ b/debug_toolbar/panels/tasks.py @@ -0,0 +1,80 @@ +import django +from django.utils.translation import gettext_lazy as _, ngettext + +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 + + 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): + """Hook into task system to collect queued tasks""" + 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")) + + # 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 + + 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): + """Restore original methods""" + try: + from django.tasks import Task + + if hasattr(self, "_original_enqueue"): + Task.enqueue = self._original_enqueue + except (ImportError, AttributeError): + pass + + @property + def _tasks_available(self) -> bool: + """Check if Django tasks system is available""" + if django.VERSION < (6, 0): + return False + return 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..bb05406ad --- /dev/null +++ b/debug_toolbar/templates/debug_toolbar/panels/tasks.html @@ -0,0 +1,16 @@ +{% load i18n %} + + + + + + + + + {% for task in tasks %} + + + + {% endfor %} + +
{% translate "Task Info" %}
{{ task }}
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") 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", }