Skip to content

Commit d37c910

Browse files
committed
Fix KeyError in DatabaseStore when dynamically adding panels and ensure from_store flag is set correctly
1 parent 501a145 commit d37c910

File tree

3 files changed

+251
-1
lines changed

3 files changed

+251
-1
lines changed

debug_toolbar/toolbar.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,9 @@ def from_store(cls, request_id, panel_id=None):
223223
data = toolbar.store.panel(toolbar.request_id, panel.panel_id)
224224
if data:
225225
panel.load_stats_from_store(data)
226-
toolbar._panels[panel.panel_id] = panel
226+
else:
227+
panel.from_store = True
228+
toolbar._panels[panel.panel_id] = panel
227229
return toolbar
228230

229231

docs/changes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Pending
66

77
* Added a note about the default password in ``make example``.
88
* Removed logging about the toolbar failing to serialize a value into JSON.
9+
* Fixed KeyError when using DatabaseStore with dynamically added panels to
10+
DEBUG_TOOLBAR_PANELS.
911

1012
6.0.0 (2025-07-22)
1113
------------------

tests/test_database_store_fix.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
"""
2+
Test for the DatabaseStore panel loading fix
3+
"""
4+
5+
from django.conf import settings
6+
from django.test import TestCase, override_settings
7+
from django.test.signals import setting_changed
8+
9+
from debug_toolbar.settings import get_panels
10+
from debug_toolbar.store import get_store
11+
from debug_toolbar.toolbar import DebugToolbar, StoredDebugToolbar
12+
13+
14+
class DatabaseStorePanelLoadingTestCase(TestCase):
15+
"""
16+
Test that StoredDebugToolbar.from_store loads all configured panels,
17+
even those that don't have stored data.
18+
19+
This fixes the KeyError issue when users dynamically add panels
20+
to DEBUG_TOOLBAR_PANELS after requests have been made.
21+
"""
22+
23+
def setUp(self):
24+
"""Clear any cached data and ensure clean state"""
25+
# Store original panels configuration
26+
self.original_panels = getattr(settings, "DEBUG_TOOLBAR_PANELS", None)
27+
28+
# Clear store data
29+
get_store().clear()
30+
31+
# Clear panel classes cache
32+
DebugToolbar._panel_classes = None
33+
StoredDebugToolbar._panel_classes = None
34+
get_panels.cache_clear()
35+
36+
# Reset to default panels to ensure consistent starting state
37+
if hasattr(settings, "DEBUG_TOOLBAR_PANELS"):
38+
delattr(settings, "DEBUG_TOOLBAR_PANELS")
39+
DebugToolbar._panel_classes = None
40+
StoredDebugToolbar._panel_classes = None
41+
get_panels.cache_clear()
42+
43+
def tearDown(self):
44+
"""Restore original state"""
45+
# Restore original panels configuration
46+
if self.original_panels is not None:
47+
settings.DEBUG_TOOLBAR_PANELS = self.original_panels
48+
elif hasattr(settings, "DEBUG_TOOLBAR_PANELS"):
49+
delattr(settings, "DEBUG_TOOLBAR_PANELS")
50+
51+
# Clear caches for both DebugToolbar and StoredDebugToolbar
52+
DebugToolbar._panel_classes = None
53+
StoredDebugToolbar._panel_classes = None
54+
get_panels.cache_clear()
55+
56+
# Clear store
57+
get_store().clear()
58+
59+
def test_stored_toolbar_loads_all_configured_panels(self):
60+
"""
61+
Test that StoredDebugToolbar.from_store loads all panels from
62+
DEBUG_TOOLBAR_PANELS, not just panels that have stored data.
63+
"""
64+
minimal_panels = [
65+
"debug_toolbar.panels.request.RequestPanel",
66+
"debug_toolbar.panels.history.HistoryPanel",
67+
"debug_toolbar.panels.timer.TimerPanel",
68+
]
69+
full_panels = [
70+
"debug_toolbar.panels.history.HistoryPanel",
71+
"debug_toolbar.panels.versions.VersionsPanel",
72+
"debug_toolbar.panels.timer.TimerPanel",
73+
"debug_toolbar.panels.settings.SettingsPanel",
74+
"debug_toolbar.panels.headers.HeadersPanel",
75+
"debug_toolbar.panels.request.RequestPanel",
76+
"debug_toolbar.panels.sql.SQLPanel",
77+
"debug_toolbar.panels.staticfiles.StaticFilesPanel",
78+
"debug_toolbar.panels.templates.TemplatesPanel",
79+
"debug_toolbar.panels.alerts.AlertsPanel",
80+
"debug_toolbar.panels.cache.CachePanel",
81+
"debug_toolbar.panels.signals.SignalsPanel",
82+
"debug_toolbar.panels.redirects.RedirectsPanel",
83+
"debug_toolbar.panels.profiling.ProfilingPanel",
84+
]
85+
86+
# Step 1: Set minimal panels
87+
settings.DEBUG_TOOLBAR_PANELS = minimal_panels
88+
DebugToolbar._panel_classes = None
89+
get_panels.cache_clear()
90+
setting_changed.send(
91+
sender=self.__class__,
92+
setting="DEBUG_TOOLBAR_PANELS",
93+
value=minimal_panels,
94+
enter=True,
95+
)
96+
97+
# Step 2: Create a toolbar and save data for minimal panels
98+
from django.test import RequestFactory
99+
100+
factory = RequestFactory()
101+
request = factory.get("/")
102+
request.META["REMOTE_ADDR"] = "127.0.0.1"
103+
104+
def dummy_response(req):
105+
from django.http import HttpResponse
106+
107+
return HttpResponse("OK")
108+
109+
toolbar = DebugToolbar(request, dummy_response)
110+
request_id = toolbar.request_id
111+
112+
# Verify we have minimal panels
113+
self.assertEqual(len(toolbar._panels), 3)
114+
self.assertIn("HistoryPanel", toolbar._panels)
115+
self.assertIn(
116+
"RequestPanel", toolbar._panels
117+
) # RequestPanel is in minimal panels
118+
self.assertNotIn(
119+
"SQLPanel", toolbar._panels
120+
) # SQLPanel is not in minimal panels
121+
122+
# Save data for the minimal panels (simulating request processing)
123+
store = get_store()
124+
for panel_id, _panel in toolbar._panels.items():
125+
dummy_data = {"test": "data", "panel": panel_id}
126+
store.save_panel(request_id, panel_id, dummy_data)
127+
128+
# Step 3: Change to full panel configuration
129+
settings.DEBUG_TOOLBAR_PANELS = full_panels
130+
DebugToolbar._panel_classes = None
131+
StoredDebugToolbar._panel_classes = None
132+
get_panels.cache_clear()
133+
setting_changed.send(
134+
sender=self.__class__,
135+
setting="DEBUG_TOOLBAR_PANELS",
136+
value=full_panels,
137+
enter=True,
138+
)
139+
140+
# Verify we now have full panels configured
141+
self.assertEqual(len(get_panels()), 14)
142+
self.assertEqual(len(DebugToolbar.get_panel_classes()), 14)
143+
144+
# Step 4: Load toolbar from store
145+
stored_toolbar = StoredDebugToolbar.from_store(request_id)
146+
147+
# Step 5: Verify ALL configured panels are loaded, not just those with data
148+
self.assertEqual(
149+
len(stored_toolbar._panels),
150+
14,
151+
f"Expected 14 panels, got {len(stored_toolbar._panels)}: {list(stored_toolbar._panels.keys())}",
152+
)
153+
154+
# Panels with stored data should be accessible and have data
155+
self.assertIn("HistoryPanel", stored_toolbar._panels)
156+
history_panel = stored_toolbar._panels["HistoryPanel"]
157+
self.assertTrue(bool(history_panel.get_stats()))
158+
159+
# Panels without stored data should still be accessible (this was the original bug)
160+
# RequestPanel actually has stored data since it was in minimal_panels
161+
self.assertIn("RequestPanel", stored_toolbar._panels)
162+
request_panel = stored_toolbar._panels["RequestPanel"]
163+
self.assertTrue(bool(request_panel.get_stats())) # Has stored data
164+
165+
# Test a panel that was NOT in minimal_panels and has no stored data
166+
self.assertIn("SQLPanel", stored_toolbar._panels)
167+
sql_panel = stored_toolbar._panels["SQLPanel"]
168+
self.assertFalse(bool(sql_panel.get_stats())) # No stored data
169+
170+
# Step 6: Verify get_panel_by_id works for all panels (this was the original bug)
171+
# This should not raise KeyError
172+
panel = stored_toolbar.get_panel_by_id("RequestPanel")
173+
self.assertIsNotNone(panel)
174+
self.assertEqual(panel.panel_id, "RequestPanel")
175+
176+
panel = stored_toolbar.get_panel_by_id("SQLPanel")
177+
self.assertIsNotNone(panel)
178+
self.assertEqual(panel.panel_id, "SQLPanel")
179+
180+
panel = stored_toolbar.get_panel_by_id("HistoryPanel")
181+
self.assertIsNotNone(panel)
182+
self.assertEqual(panel.panel_id, "HistoryPanel")
183+
184+
def test_stored_toolbar_from_store_preserves_from_store_flag(self):
185+
"""
186+
Test that panels loaded from store have from_store=True even without data.
187+
This prevents the enabled property from trying to access request.COOKIES.
188+
"""
189+
# Use minimal panels first, then expand
190+
minimal_panels = ["debug_toolbar.panels.history.HistoryPanel"]
191+
full_panels = [
192+
"debug_toolbar.panels.history.HistoryPanel",
193+
"debug_toolbar.panels.request.RequestPanel",
194+
]
195+
196+
with override_settings(DEBUG_TOOLBAR_PANELS=minimal_panels):
197+
DebugToolbar._panel_classes = None
198+
get_panels.cache_clear()
199+
setting_changed.send(
200+
sender=self.__class__,
201+
setting="DEBUG_TOOLBAR_PANELS",
202+
value=minimal_panels,
203+
enter=True,
204+
)
205+
206+
from django.test import RequestFactory
207+
208+
factory = RequestFactory()
209+
request = factory.get("/")
210+
request.META["REMOTE_ADDR"] = "127.0.0.1"
211+
212+
def dummy_response(req):
213+
from django.http import HttpResponse
214+
215+
return HttpResponse("OK")
216+
217+
toolbar = DebugToolbar(request, dummy_response)
218+
request_id = toolbar.request_id
219+
220+
# Save data only for HistoryPanel
221+
store = get_store()
222+
store.save_panel(request_id, "HistoryPanel", {"test": "data"})
223+
224+
with override_settings(DEBUG_TOOLBAR_PANELS=full_panels):
225+
DebugToolbar._panel_classes = None
226+
get_panels.cache_clear()
227+
setting_changed.send(
228+
sender=self.__class__,
229+
setting="DEBUG_TOOLBAR_PANELS",
230+
value=full_panels,
231+
enter=True,
232+
)
233+
234+
stored_toolbar = StoredDebugToolbar.from_store(request_id)
235+
236+
# Both panels should have from_store=True
237+
history_panel = stored_toolbar._panels["HistoryPanel"]
238+
self.assertTrue(history_panel.from_store)
239+
240+
request_panel = stored_toolbar._panels["RequestPanel"]
241+
self.assertTrue(request_panel.from_store)
242+
243+
# This should not raise AttributeError about request.COOKIES being None
244+
# because from_store=True causes enabled to return bool(get_stats())
245+
self.assertTrue(history_panel.enabled) # Has stats
246+
self.assertFalse(request_panel.enabled) # No stats

0 commit comments

Comments
 (0)