toolbar.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. """
  2. The main DebugToolbar class that loads and renders the Toolbar.
  3. """
  4. import uuid
  5. from collections import OrderedDict
  6. from functools import lru_cache
  7. from django.apps import apps
  8. from django.core.exceptions import ImproperlyConfigured
  9. from django.dispatch import Signal
  10. from django.template import TemplateSyntaxError
  11. from django.template.loader import render_to_string
  12. from django.urls import path, resolve
  13. from django.urls.exceptions import Resolver404
  14. from django.utils.module_loading import import_string
  15. from django.utils.translation import get_language, override as lang_override
  16. from debug_toolbar import APP_NAME, settings as dt_settings
  17. class DebugToolbar:
  18. # for internal testing use only
  19. _created = Signal()
  20. def __init__(self, request, get_response):
  21. self.request = request
  22. self.config = dt_settings.get_config().copy()
  23. panels = []
  24. for panel_class in reversed(self.get_panel_classes()):
  25. panel = panel_class(self, get_response)
  26. panels.append(panel)
  27. if panel.enabled:
  28. get_response = panel.process_request
  29. self.process_request = get_response
  30. # Use OrderedDict for the _panels attribute so that items can be efficiently
  31. # removed using FIFO order in the DebugToolbar.store() method. The .popitem()
  32. # method of Python's built-in dict only supports LIFO removal.
  33. self._panels = OrderedDict()
  34. while panels:
  35. panel = panels.pop()
  36. self._panels[panel.panel_id] = panel
  37. self.stats = {}
  38. self.server_timing_stats = {}
  39. self.store_id = None
  40. self._created.send(request, toolbar=self)
  41. # Manage panels
  42. @property
  43. def panels(self):
  44. """
  45. Get a list of all available panels.
  46. """
  47. return list(self._panels.values())
  48. @property
  49. def enabled_panels(self):
  50. """
  51. Get a list of panels enabled for the current request.
  52. """
  53. return [panel for panel in self._panels.values() if panel.enabled]
  54. def get_panel_by_id(self, panel_id):
  55. """
  56. Get the panel with the given id, which is the class name by default.
  57. """
  58. return self._panels[panel_id]
  59. # Handle rendering the toolbar in HTML
  60. def render_toolbar(self):
  61. """
  62. Renders the overall Toolbar with panels inside.
  63. """
  64. if not self.should_render_panels():
  65. self.store()
  66. try:
  67. context = {"toolbar": self}
  68. lang = self.config["TOOLBAR_LANGUAGE"] or get_language()
  69. with lang_override(lang):
  70. return render_to_string("debug_toolbar/base.html", context)
  71. except TemplateSyntaxError:
  72. if not apps.is_installed("django.contrib.staticfiles"):
  73. raise ImproperlyConfigured(
  74. "The debug toolbar requires the staticfiles contrib app. "
  75. "Add 'django.contrib.staticfiles' to INSTALLED_APPS and "
  76. "define STATIC_URL in your settings."
  77. )
  78. else:
  79. raise
  80. def should_render_panels(self):
  81. """Determine whether the panels should be rendered during the request
  82. If False, the panels will be loaded via Ajax.
  83. """
  84. if (render_panels := self.config["RENDER_PANELS"]) is None:
  85. # If wsgi.multiprocess isn't in the headers, then it's likely
  86. # being served by ASGI. This type of set up is most likely
  87. # incompatible with the toolbar until
  88. # https://github.com/jazzband/django-debug-toolbar/issues/1430
  89. # is resolved.
  90. render_panels = self.request.META.get("wsgi.multiprocess", True)
  91. return render_panels
  92. # Handle storing toolbars in memory and fetching them later on
  93. _store = OrderedDict()
  94. def store(self):
  95. # Store already exists.
  96. if self.store_id:
  97. return
  98. self.store_id = uuid.uuid4().hex
  99. self._store[self.store_id] = self
  100. for _ in range(self.config["RESULTS_CACHE_SIZE"], len(self._store)):
  101. self._store.popitem(last=False)
  102. @classmethod
  103. def fetch(cls, store_id):
  104. return cls._store.get(store_id)
  105. # Manually implement class-level caching of panel classes and url patterns
  106. # because it's more obvious than going through an abstraction.
  107. _panel_classes = None
  108. @classmethod
  109. def get_panel_classes(cls):
  110. if cls._panel_classes is None:
  111. # Load panels in a temporary variable for thread safety.
  112. panel_classes = [
  113. import_string(panel_path) for panel_path in dt_settings.get_panels()
  114. ]
  115. cls._panel_classes = panel_classes
  116. return cls._panel_classes
  117. _urlpatterns = None
  118. @classmethod
  119. def get_urls(cls):
  120. if cls._urlpatterns is None:
  121. from . import views
  122. # Load URLs in a temporary variable for thread safety.
  123. # Global URLs
  124. urlpatterns = [
  125. path("render_panel/", views.render_panel, name="render_panel"),
  126. ]
  127. # Per-panel URLs
  128. for panel_class in cls.get_panel_classes():
  129. urlpatterns += panel_class.get_urls()
  130. cls._urlpatterns = urlpatterns
  131. return cls._urlpatterns
  132. @classmethod
  133. def is_toolbar_request(cls, request):
  134. """
  135. Determine if the request is for a DebugToolbar view.
  136. """
  137. # The primary caller of this function is in the middleware which may
  138. # not have resolver_match set.
  139. try:
  140. resolver_match = request.resolver_match or resolve(
  141. request.path, getattr(request, "urlconf", None)
  142. )
  143. except Resolver404:
  144. return False
  145. return resolver_match.namespaces and resolver_match.namespaces[-1] == APP_NAME
  146. @staticmethod
  147. @lru_cache(maxsize=None)
  148. def get_observe_request():
  149. # If OBSERVE_REQUEST_CALLBACK is a string, which is the recommended
  150. # setup, resolve it to the corresponding callable.
  151. func_or_path = dt_settings.get_config()["OBSERVE_REQUEST_CALLBACK"]
  152. if isinstance(func_or_path, str):
  153. return import_string(func_or_path)
  154. else:
  155. return func_or_path
  156. def observe_request(request):
  157. """
  158. Determine whether to update the toolbar from a client side request.
  159. """
  160. return not DebugToolbar.is_toolbar_request(request)