189 lines
6.4 KiB
Python
189 lines
6.4 KiB
Python
|
"""
|
||
|
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)
|