Skip to content

Commit f32d8d9

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

File tree

3 files changed

+235
-1
lines changed

3 files changed

+235
-1
lines changed

debug_toolbar/toolbar.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,11 @@ 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+
# Mark panel as loaded from store even if no data exists
228+
# This prevents enabled property from trying to access request.COOKIES
229+
panel.from_store = True
230+
toolbar._panels[panel.panel_id] = panel
227231
return toolbar
228232

229233

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

0 commit comments

Comments
 (0)