Skip to content

Commit bbbbb34

Browse files
committed
Support serializable panels. This is a WIP and needs clean-up.
The remainder of the work is to fix the individual panels' serialization errors.
1 parent c4201fa commit bbbbb34

File tree

15 files changed

+148
-65
lines changed

15 files changed

+148
-65
lines changed

debug_toolbar/panels/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Panel:
1212
def __init__(self, toolbar, get_response):
1313
self.toolbar = toolbar
1414
self.get_response = get_response
15+
self.from_store = False
1516

1617
# Private panel properties
1718

@@ -21,6 +22,12 @@ def panel_id(self):
2122

2223
@property
2324
def enabled(self) -> bool:
25+
if self.from_store:
26+
# If the toolbar was loaded from the store the existence of
27+
# recorded data indicates whether it was enabled or not.
28+
# We can't use the remainder of the logic since we don't have
29+
# a request to work off of.
30+
return bool(self.get_stats())
2431
# The user's cookies should override the default value
2532
cookie_value = self.toolbar.request.COOKIES.get("djdt" + self.panel_id)
2633
if cookie_value is not None:
@@ -168,6 +175,9 @@ def record_stats(self, stats):
168175
Each call to ``record_stats`` updates the statistics dictionary.
169176
"""
170177
self.toolbar.stats.setdefault(self.panel_id, {}).update(stats)
178+
self.toolbar.store.save_panel(
179+
self.toolbar.request_id, self.panel_id, self.toolbar.stats[self.panel_id]
180+
)
171181

172182
def get_stats(self):
173183
"""
@@ -251,6 +261,15 @@ def generate_server_timing(self, request, response):
251261
Does not return a value.
252262
"""
253263

264+
def load_stats_from_store(self, data):
265+
"""
266+
Instantiate the panel from serialized data.
267+
268+
Return the panel instance.
269+
"""
270+
self.toolbar.stats.setdefault(self.panel_id, {}).update(data)
271+
self.from_store = True
272+
254273
@classmethod
255274
def run_checks(cls):
256275
"""

debug_toolbar/panels/history/panel.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,19 @@ def content(self):
8686
8787
Fetch every store for the toolbar and include it in the template.
8888
"""
89-
stores = {}
90-
for id, toolbar in reversed(self.toolbar._store.items()):
91-
stores[id] = {
92-
"toolbar": toolbar,
89+
toolbar_history = {}
90+
for request_id in reversed(self.toolbar.store.request_ids()):
91+
toolbar_history[request_id] = {
9392
"form": HistoryStoreForm(
94-
initial={"request_id": id, "exclude_history": True}
93+
initial={"request_id": request_id, "exclude_history": True}
9594
),
9695
}
9796

9897
return render_to_string(
9998
self.template,
10099
{
101100
"current_request_id": self.toolbar.request_id,
102-
"stores": stores,
101+
"toolbar_history": toolbar_history,
103102
"refresh_form": HistoryStoreForm(
104103
initial={
105104
"request_id": self.toolbar.request_id,

debug_toolbar/panels/history/views.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar
55
from debug_toolbar.panels.history.forms import HistoryStoreForm
6+
from debug_toolbar.store import get_store
67
from debug_toolbar.toolbar import DebugToolbar
78

89

@@ -46,19 +47,20 @@ def history_refresh(request):
4647
if form.is_valid():
4748
requests = []
4849
# Convert to list to handle mutations happening in parallel
49-
for id, toolbar in list(DebugToolbar._store.items()):
50+
for request_id in get_store().request_ids():
51+
toolbar = DebugToolbar.fetch(request_id)
5052
requests.append(
5153
{
52-
"id": id,
54+
"id": request_id,
5355
"content": render_to_string(
5456
"debug_toolbar/panels/history_tr.html",
5557
{
56-
"id": id,
58+
"id": request_id,
5759
"store_context": {
5860
"toolbar": toolbar,
5961
"form": HistoryStoreForm(
6062
initial={
61-
"request_id": id,
63+
"request_id": request_id,
6264
"exclude_history": True,
6365
}
6466
),

debug_toolbar/panels/settings.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.conf import settings
2+
from django.utils.encoding import force_str
23
from django.utils.translation import gettext_lazy as _
34
from django.views.debug import get_default_exception_reporter_filter
45

@@ -20,4 +21,11 @@ def title(self):
2021
return _("Settings from %s") % settings.SETTINGS_MODULE
2122

2223
def generate_stats(self, request, response):
23-
self.record_stats({"settings": dict(sorted(get_safe_settings().items()))})
24+
self.record_stats(
25+
{
26+
"settings": {
27+
key: force_str(value)
28+
for key, value in sorted(get_safe_settings().items())
29+
}
30+
}
31+
)

debug_toolbar/panels/templates/panel.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.test.signals import template_rendered
1010
from django.test.utils import instrumented_test_render
1111
from django.urls import path
12+
from django.utils.encoding import force_str
1213
from django.utils.translation import gettext_lazy as _
1314

1415
from debug_toolbar.panels import Panel
@@ -179,7 +180,7 @@ def generate_stats(self, request, response):
179180
else:
180181
template.origin_name = _("No origin")
181182
template.origin_hash = ""
182-
info["template"] = template
183+
info["template"] = force_str(template)
183184
# Clean up context for better readability
184185
if self.toolbar.config["SHOW_TEMPLATE_CONTEXT"]:
185186
context_list = template_data.get("context", [])
@@ -188,7 +189,14 @@ def generate_stats(self, request, response):
188189

189190
# Fetch context_processors/template_dirs from any template
190191
if self.templates:
191-
context_processors = self.templates[0]["context_processors"]
192+
context_processors = (
193+
{
194+
key: force_str(value)
195+
for key, value in self.templates[0]["context_processors"].items()
196+
}
197+
if self.templates[0]["context_processors"]
198+
else None
199+
)
192200
template = self.templates[0]["template"]
193201
# django templates have the 'engine' attribute, while jinja
194202
# templates use 'backend'

debug_toolbar/store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,5 +126,5 @@ def panel(cls, request_id: str, panel_id: str) -> Any:
126126
return deserialize(data)
127127

128128

129-
def get_store():
129+
def get_store() -> BaseStore:
130130
return import_string(dt_settings.get_config()["TOOLBAR_STORE_CLASS"])

debug_toolbar/templates/debug_toolbar/panels/history.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</tr>
1616
</thead>
1717
<tbody id="djdtHistoryRequests">
18-
{% for id, store_context in stores.items %}
18+
{% for request_id, store_context in toolbar_history.items %}
1919
{% include "debug_toolbar/panels/history_tr.html" %}
2020
{% endfor %}
2121
</tbody>

debug_toolbar/templates/debug_toolbar/panels/history_tr.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% load i18n %}
2-
<tr class="{% if id == current_request_id %}djdt-highlighted{% endif %}" id="historyMain_{{ id }}" data-request-id="{{ id }}">
2+
<tr class="{% if request_id == current_request_id %}djdt-highlighted{% endif %}" id="historyMain_{{ request_id }}" data-request-id="{{ request_id }}">
33
<td>
44
{{ store_context.toolbar.stats.HistoryPanel.time|escape }}
55
</td>
@@ -10,8 +10,8 @@
1010
<p>{{ store_context.toolbar.stats.HistoryPanel.request_url|truncatechars:100|escape }}</p>
1111
</td>
1212
<td>
13-
<button type="button" class="djToggleSwitch" data-toggle-name="historyMain" data-toggle-id="{{ id }}">+</button>
14-
<div class="djUnselected djToggleDetails_{{ id }}">
13+
<button type="button" class="djToggleSwitch" data-toggle-name="historyMain" data-toggle-id="{{ request_id }}">+</button>
14+
<div class="djUnselected djToggleDetails_{{ request_id }}">
1515
<table>
1616
<colgroup>
1717
<col class="djdt-width-20">
@@ -44,7 +44,7 @@
4444
<td class="djdt-actions">
4545
<form method="get" action="{% url 'djdt:history_sidebar' %}">
4646
{{ store_context.form }}
47-
<button data-request-id="{{ id }}" class="switchHistory">Switch</button>
47+
<button data-request-id="{{ request_id }}" class="switchHistory">Switch</button>
4848
</form>
4949
</td>
5050
</tr>

debug_toolbar/toolbar.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""
22
The main DebugToolbar class that loads and renders the Toolbar.
33
"""
4-
4+
import logging
55
import uuid
6-
from collections import OrderedDict
76
from functools import lru_cache
87

98
from django.apps import apps
@@ -17,13 +16,17 @@
1716
from django.utils.translation import get_language, override as lang_override
1817

1918
from debug_toolbar import APP_NAME, settings as dt_settings
19+
from debug_toolbar.store import get_store
20+
21+
logger = logging.getLogger(__name__)
2022

2123

2224
class DebugToolbar:
2325
# for internal testing use only
2426
_created = Signal()
27+
store = None
2528

26-
def __init__(self, request, get_response):
29+
def __init__(self, request, get_response, request_id=None):
2730
self.request = request
2831
self.config = dt_settings.get_config().copy()
2932
panels = []
@@ -33,16 +36,11 @@ def __init__(self, request, get_response):
3336
if panel.enabled:
3437
get_response = panel.process_request
3538
self.process_request = get_response
36-
# Use OrderedDict for the _panels attribute so that items can be efficiently
37-
# removed using FIFO order in the DebugToolbar.store() method. The .popitem()
38-
# method of Python's built-in dict only supports LIFO removal.
39-
self._panels = OrderedDict()
40-
while panels:
41-
panel = panels.pop()
42-
self._panels[panel.panel_id] = panel
39+
self._panels = {panel.panel_id: panel for panel in reversed(panels)}
4340
self.stats = {}
4441
self.server_timing_stats = {}
45-
self.request_id = None
42+
self.request_id = request_id
43+
self.init_store()
4644
self._created.send(request, toolbar=self)
4745

4846
# Manage panels
@@ -74,7 +72,7 @@ def render_toolbar(self):
7472
Renders the overall Toolbar with panels inside.
7573
"""
7674
if not self.should_render_panels():
77-
self.store()
75+
self.init_store()
7876
try:
7977
context = {"toolbar": self}
8078
lang = self.config["TOOLBAR_LANGUAGE"] or get_language()
@@ -106,20 +104,20 @@ def should_render_panels(self):
106104

107105
# Handle storing toolbars in memory and fetching them later on
108106

109-
_store = OrderedDict()
107+
def init_store(self):
108+
# Store already initialized.
109+
if self.store is None:
110+
self.store = get_store()
110111

111-
def store(self):
112-
# Store already exists.
113112
if self.request_id:
114113
return
115114
self.request_id = uuid.uuid4().hex
116-
self._store[self.request_id] = self
117-
for _ in range(self.config["RESULTS_CACHE_SIZE"], len(self._store)):
118-
self._store.popitem(last=False)
115+
self.store.set(self.request_id)
119116

120117
@classmethod
121-
def fetch(cls, request_id):
122-
return cls._store.get(request_id)
118+
def fetch(cls, request_id, panel_id=None):
119+
if get_store().exists(request_id):
120+
return StoredDebugToolbar.from_store(request_id, panel_id=panel_id)
123121

124122
# Manually implement class-level caching of panel classes and url patterns
125123
# because it's more obvious than going through an abstraction.
@@ -186,3 +184,38 @@ def observe_request(request):
186184
Determine whether to update the toolbar from a client side request.
187185
"""
188186
return not DebugToolbar.is_toolbar_request(request)
187+
188+
189+
def from_store_get_response(request):
190+
logger.warning(
191+
"get_response was called for debug toolbar after being loaded from the store. No request exists in this scenario as the request is not stored, only the panel's data."
192+
)
193+
return None
194+
195+
196+
class StoredDebugToolbar(DebugToolbar):
197+
def __init__(self, request, get_response, request_id=None):
198+
self.request = None
199+
self.config = dt_settings.get_config().copy()
200+
self.process_request = get_response
201+
self.stats = {}
202+
self.server_timing_stats = {}
203+
self.request_id = request_id
204+
self.init_store()
205+
206+
@classmethod
207+
def from_store(cls, request_id, panel_id=None):
208+
toolbar = StoredDebugToolbar(
209+
None, from_store_get_response, request_id=request_id
210+
)
211+
toolbar._panels = {}
212+
213+
for panel_class in reversed(cls.get_panel_classes()):
214+
panel = panel_class(toolbar, from_store_get_response)
215+
if panel_id and panel.panel_id != panel_id:
216+
continue
217+
data = toolbar.store.panel(toolbar.request_id, panel.panel_id)
218+
if data:
219+
panel.load_stats_from_store(data)
220+
toolbar._panels[panel.panel_id] = panel
221+
return toolbar

debug_toolbar/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@render_with_toolbar_language
1111
def render_panel(request):
1212
"""Render the contents of a panel"""
13-
toolbar = DebugToolbar.fetch(request.GET["request_id"])
13+
toolbar = DebugToolbar.fetch(request.GET["request_id"], request.GET["panel_id"])
1414
if toolbar is None:
1515
content = _(
1616
"Data for this panel isn't available anymore. "

0 commit comments

Comments
 (0)