123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- """
- The main DebugToolbar class that loads and renders the Toolbar.
- """
- import uuid
- from collections import OrderedDict
- from functools import lru_cache
- from django.apps import apps
- from django.core.exceptions import ImproperlyConfigured
- from django.dispatch import Signal
- from django.template import TemplateSyntaxError
- from django.template.loader import render_to_string
- from django.urls import path, resolve
- from django.urls.exceptions import Resolver404
- from django.utils.module_loading import import_string
- from django.utils.translation import get_language, override as lang_override
- from debug_toolbar import APP_NAME, settings as dt_settings
- class DebugToolbar:
- # for internal testing use only
- _created = Signal()
- def __init__(self, request, get_response):
- self.request = request
- self.config = dt_settings.get_config().copy()
- panels = []
- for panel_class in reversed(self.get_panel_classes()):
- panel = panel_class(self, get_response)
- panels.append(panel)
- if panel.enabled:
- get_response = panel.process_request
- self.process_request = get_response
- # Use OrderedDict for the _panels attribute so that items can be efficiently
- # removed using FIFO order in the DebugToolbar.store() method. The .popitem()
- # method of Python's built-in dict only supports LIFO removal.
- self._panels = OrderedDict()
- while panels:
- panel = panels.pop()
- self._panels[panel.panel_id] = panel
- self.stats = {}
- self.server_timing_stats = {}
- self.store_id = None
- self._created.send(request, toolbar=self)
- # Manage panels
- @property
- def panels(self):
- """
- Get a list of all available panels.
- """
- return list(self._panels.values())
- @property
- def enabled_panels(self):
- """
- Get a list of panels enabled for the current request.
- """
- return [panel for panel in self._panels.values() if panel.enabled]
- def get_panel_by_id(self, panel_id):
- """
- Get the panel with the given id, which is the class name by default.
- """
- return self._panels[panel_id]
- # Handle rendering the toolbar in HTML
- def render_toolbar(self):
- """
- Renders the overall Toolbar with panels inside.
- """
- if not self.should_render_panels():
- self.store()
- try:
- context = {"toolbar": self}
- lang = self.config["TOOLBAR_LANGUAGE"] or get_language()
- with lang_override(lang):
- return render_to_string("debug_toolbar/base.html", context)
- except TemplateSyntaxError:
- if not apps.is_installed("django.contrib.staticfiles"):
- raise ImproperlyConfigured(
- "The debug toolbar requires the staticfiles contrib app. "
- "Add 'django.contrib.staticfiles' to INSTALLED_APPS and "
- "define STATIC_URL in your settings."
- )
- else:
- raise
- def should_render_panels(self):
- """Determine whether the panels should be rendered during the request
- If False, the panels will be loaded via Ajax.
- """
- if (render_panels := self.config["RENDER_PANELS"]) is None:
- # If wsgi.multiprocess isn't in the headers, then it's likely
- # being served by ASGI. This type of set up is most likely
- # incompatible with the toolbar until
- # https://github.com/jazzband/django-debug-toolbar/issues/1430
- # is resolved.
- render_panels = self.request.META.get("wsgi.multiprocess", True)
- return render_panels
- # Handle storing toolbars in memory and fetching them later on
- _store = OrderedDict()
- def store(self):
- # Store already exists.
- if self.store_id:
- return
- self.store_id = uuid.uuid4().hex
- self._store[self.store_id] = self
- for _ in range(self.config["RESULTS_CACHE_SIZE"], len(self._store)):
- self._store.popitem(last=False)
- @classmethod
- def fetch(cls, store_id):
- return cls._store.get(store_id)
- # Manually implement class-level caching of panel classes and url patterns
- # because it's more obvious than going through an abstraction.
- _panel_classes = None
- @classmethod
- def get_panel_classes(cls):
- if cls._panel_classes is None:
- # Load panels in a temporary variable for thread safety.
- panel_classes = [
- import_string(panel_path) for panel_path in dt_settings.get_panels()
- ]
- cls._panel_classes = panel_classes
- return cls._panel_classes
- _urlpatterns = None
- @classmethod
- def get_urls(cls):
- if cls._urlpatterns is None:
- from . import views
- # Load URLs in a temporary variable for thread safety.
- # Global URLs
- urlpatterns = [
- path("render_panel/", views.render_panel, name="render_panel"),
- ]
- # Per-panel URLs
- for panel_class in cls.get_panel_classes():
- urlpatterns += panel_class.get_urls()
- cls._urlpatterns = urlpatterns
- return cls._urlpatterns
- @classmethod
- def is_toolbar_request(cls, request):
- """
- Determine if the request is for a DebugToolbar view.
- """
- # The primary caller of this function is in the middleware which may
- # not have resolver_match set.
- try:
- resolver_match = request.resolver_match or resolve(
- request.path, getattr(request, "urlconf", None)
- )
- except Resolver404:
- return False
- return resolver_match.namespaces and resolver_match.namespaces[-1] == APP_NAME
- @staticmethod
- @lru_cache(maxsize=None)
- def get_observe_request():
- # If OBSERVE_REQUEST_CALLBACK is a string, which is the recommended
- # setup, resolve it to the corresponding callable.
- func_or_path = dt_settings.get_config()["OBSERVE_REQUEST_CALLBACK"]
- if isinstance(func_or_path, str):
- return import_string(func_or_path)
- else:
- return func_or_path
- def observe_request(request):
- """
- Determine whether to update the toolbar from a client side request.
- """
- return not DebugToolbar.is_toolbar_request(request)
|