1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
|
from django.core.handlers.asgi import ASGIRequest
from django.template.loader import render_to_string
from debug_toolbar import settings as dt_settings
from debug_toolbar.utils import get_name_from_obj
class Panel:
"""
Base class for panels.
"""
is_async = False
def __init__(self, toolbar, get_response):
self.toolbar = toolbar
self.get_response = get_response
# Private panel properties
@property
def panel_id(self):
return self.__class__.__name__
@property
def enabled(self) -> bool:
# check if the panel is async compatible
if not self.is_async and isinstance(self.toolbar.request, ASGIRequest):
return False
# The user's cookies should override the default value
cookie_value = self.toolbar.request.COOKIES.get("djdt" + self.panel_id)
if cookie_value is not None:
return cookie_value == "on"
# Check to see if settings has a default value for it
disabled_panels = dt_settings.get_config()["DISABLE_PANELS"]
panel_path = get_name_from_obj(self)
# Some panels such as the SQLPanel and TemplatesPanel exist in a
# panel module, but can be disabled without panel in the path.
# For that reason, replace .panel. in the path and check for that
# value in the disabled panels as well.
return (
panel_path not in disabled_panels
and panel_path.replace(".panel.", ".") not in disabled_panels
)
# Titles and content
@property
def nav_title(self):
"""
Title shown in the side bar. Defaults to :attr:`title`.
"""
return self.title
@property
def nav_subtitle(self):
"""
Subtitle shown in the side bar. Defaults to the empty string.
"""
return ""
@property
def has_content(self):
"""
``True`` if the panel can be displayed in full screen, ``False`` if
it's only shown in the side bar. Defaults to ``True``.
"""
return True
@property
def is_historical(self):
"""
Panel supports rendering historical values.
Defaults to :attr:`has_content`.
"""
return self.has_content
@property
def title(self):
"""
Title shown in the panel when it's displayed in full screen.
Mandatory, unless the panel sets :attr:`has_content` to ``False``.
"""
raise NotImplementedError
@property
def template(self):
"""
Template used to render :attr:`content`.
Mandatory, unless the panel sets :attr:`has_content` to ``False`` or
overrides :attr:`content`.
"""
raise NotImplementedError
@property
def content(self):
"""
Content of the panel when it's displayed in full screen.
By default this renders the template defined by :attr:`template`.
Statistics stored with :meth:`record_stats` are available in the
template's context.
"""
if self.has_content:
return render_to_string(self.template, self.get_stats())
@property
def scripts(self):
"""
Scripts used by the HTML content of the panel when it's displayed.
When a panel is rendered on the frontend, the ``djdt.panel.render``
JavaScript event will be dispatched. The scripts can listen for
this event to support dynamic functionality.
"""
return []
# Panel early initialization
@classmethod
def ready(cls):
"""
Perform early initialization for the panel.
This should only include initialization or instrumentation that needs to
be done unconditionally for the panel regardless of whether it is
enabled for a particular request. It should be idempotent.
"""
# URLs for panel-specific views
@classmethod
def get_urls(cls):
"""
Return URLpatterns, if the panel has its own views.
"""
return []
# Enable and disable (expensive) instrumentation, must be idempotent
def enable_instrumentation(self):
"""
Enable instrumentation to gather data for this panel.
This usually means monkey-patching (!) or registering signal
receivers. Any instrumentation with a non-negligible effect on
performance should be installed by this method rather than at import
time.
Unless the toolbar or this panel is disabled, this method will be
called early in ``DebugToolbarMiddleware``. It should be idempotent.
Add the ``aenable_instrumentation`` method to a panel subclass
to support async logic for instrumentation.
"""
def disable_instrumentation(self):
"""
Disable instrumentation to gather data for this panel.
This is the opposite of :meth:`enable_instrumentation`.
Unless the toolbar or this panel is disabled, this method will be
called late in the middleware. It should be idempotent.
"""
# Store and retrieve stats (shared between panels for no good reason)
def record_stats(self, stats):
"""
Store data gathered by the panel. ``stats`` is a :class:`dict`.
Each call to ``record_stats`` updates the statistics dictionary.
"""
self.toolbar.stats.setdefault(self.panel_id, {}).update(stats)
def get_stats(self):
"""
Access data stored by the panel. Returns a :class:`dict`.
"""
return self.toolbar.stats.get(self.panel_id, {})
def record_server_timing(self, key, title, value):
"""
Store data gathered by the panel. ``stats`` is a :class:`dict`.
Each call to ``record_stats`` updates the statistics dictionary.
"""
data = {key: {"title": title, "value": value}}
self.toolbar.server_timing_stats.setdefault(self.panel_id, {}).update(data)
def get_server_timing_stats(self):
"""
Access data stored by the panel. Returns a :class:`dict`.
"""
return self.toolbar.server_timing_stats.get(self.panel_id, {})
# Standard middleware methods
def process_request(self, request):
"""
Like __call__ in Django's middleware.
Write panel logic related to the request there. Save data with
:meth:`record_stats`.
Return the existing response or overwrite it.
"""
return self.get_response(request)
def get_headers(self, request):
"""
Get headers the panel needs to set.
Called after :meth:`process_request
<debug_toolbar.panels.Panel.generate_stats>` and
:meth:`process_request<debug_toolbar.panels.Panel.generate_stats>`
Header values will be appended if multiple panels need to set it.
By default it sets the Server-Timing header.
Return dict of headers to be appended.
"""
headers = {}
stats = self.get_server_timing_stats()
if stats:
headers["Server-Timing"] = ", ".join(
# example: `SQLPanel_sql_time;dur=0;desc="SQL 0 queries"`
'{}_{};dur={};desc="{}"'.format(
self.panel_id, key, record.get("value"), record.get("title")
)
for key, record in stats.items()
)
return headers
def generate_stats(self, request, response):
"""
Write panel logic related to the response there. Post-process data
gathered while the view executed. Save data with :meth:`record_stats`.
Called after :meth:`process_request
<debug_toolbar.panels.Panel.process_request>`.
Does not return a value.
"""
def generate_server_timing(self, request, response):
"""
Similar to :meth:`generate_stats
<debug_toolbar.panels.Panel.generate_stats>`,
Generate stats for Server Timing https://w3c.github.io/server-timing/
Does not return a value.
"""
@classmethod
def run_checks(cls):
"""
Check that the integration is configured correctly for the panel.
This will be called as a part of the Django checks system when the
application is being setup.
Return a list of :class:`django.core.checks.CheckMessage` instances.
"""
return []
|