Skip to content

Commit 9973872

Browse files
gonetim-schilling
authored andcommitted
Adding Update on ajax feature
This adds a feature to allow the toolbar to watch for ajax requests and automatically update debug information to show ajax details Adding debounce for pulling new panel info Adding heuristic function for observing requests Also adding generate_headers function Adding option to include historical data, remove signature Updated header name to match existing djdt namespace Move header value creation into the panels. This moves more logic into the Panel class and gives greater control to the Panel subclasses on how things should work. Move get_observe_request to DebugToolbar This avoids having to import the function within a panel. Rename do_not_include_history to exclude_history Add maxsize for lru_cache for python3.7 Clean up history.js documentation and remove unnecessary return.
1 parent 5d03c53 commit 9973872

File tree

15 files changed

+195
-57
lines changed

15 files changed

+195
-57
lines changed

debug_toolbar/middleware.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,13 @@ def __call__(self, request):
6767
panel.generate_stats(request, response)
6868
panel.generate_server_timing(request, response)
6969

70-
response = self.generate_server_timing_header(response, toolbar.enabled_panels)
71-
7270
# Always render the toolbar for the history panel, even if it is not
7371
# included in the response.
7472
rendered = toolbar.render_toolbar()
7573

74+
for header, value in self.get_headers(request, toolbar.enabled_panels).items():
75+
response.headers[header] = value
76+
7677
# Check for responses where the toolbar can't be inserted.
7778
content_encoding = response.get("Content-Encoding", "")
7879
content_type = response.get("Content-Type", "").split(";")[0]
@@ -96,22 +97,12 @@ def __call__(self, request):
9697
return response
9798

9899
@staticmethod
99-
def generate_server_timing_header(response, panels):
100-
data = []
101-
100+
def get_headers(request, panels):
101+
headers = {}
102102
for panel in panels:
103-
stats = panel.get_server_timing_stats()
104-
if not stats:
105-
continue
106-
107-
for key, record in stats.items():
108-
# example: `SQLPanel_sql_time;dur=0;desc="SQL 0 queries"`
109-
data.append(
110-
'{}_{};dur={};desc="{}"'.format(
111-
panel.panel_id, key, record.get("value"), record.get("title")
112-
)
113-
)
114-
115-
if data:
116-
response["Server-Timing"] = ", ".join(data)
117-
return response
103+
for header, value in panel.get_headers(request).items():
104+
if header in headers:
105+
headers[header] += f", {value}"
106+
else:
107+
headers[header] = value
108+
return headers

debug_toolbar/panels/__init__.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,41 @@ def process_request(self, request):
192192
"""
193193
return self.get_response(request)
194194

195-
def generate_stats(self, request, response):
195+
def get_headers(self, request):
196196
"""
197+
Get headers the panel needs to set.
198+
197199
Called after :meth:`process_request
198-
<debug_toolbar.panels.Panel.process_request>`, but may not be executed
199-
on every request. This will only be called if the toolbar will be
200-
inserted into the request.
200+
<debug_toolbar.panels.Panel.generate_stats>` and
201+
:meth:`process_request<debug_toolbar.panels.Panel.generate_stats>`
202+
203+
Header values will be appended if multiple panels need to set it.
204+
205+
By default it sets the Server-Timing header.
206+
207+
Return dict of headers to be appended.
208+
"""
209+
headers = {}
210+
stats = self.get_server_timing_stats()
211+
if stats:
212+
headers["Server-Timing"] = ", ".join(
213+
# example: `SQLPanel_sql_time;dur=0;desc="SQL 0 queries"`
214+
'{}_{};dur={};desc="{}"'.format(
215+
self.panel_id, key, record.get("value"), record.get("title")
216+
)
217+
for key, record in stats.items()
218+
)
219+
return headers
201220

221+
def generate_stats(self, request, response):
222+
"""
202223
Write panel logic related to the response there. Post-process data
203224
gathered while the view executed. Save data with :meth:`record_stats`.
204225
226+
Called after :meth:`process_request
227+
<debug_toolbar.panels.Panel.process_request>`.
228+
229+
205230
Does not return a value.
206231
"""
207232

debug_toolbar/panels/history/forms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ class HistoryStoreForm(forms.Form):
99
"""
1010

1111
store_id = forms.CharField(widget=forms.HiddenInput())
12+
exclude_history = forms.BooleanField(widget=forms.HiddenInput(), required=False)

debug_toolbar/panels/history/panel.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ class HistoryPanel(Panel):
2020
nav_title = _("History")
2121
template = "debug_toolbar/panels/history.html"
2222

23+
def get_headers(self, request):
24+
headers = super().get_headers(request)
25+
observe_request = self.toolbar.get_observe_request()
26+
store_id = getattr(self.toolbar, "store_id")
27+
if store_id and observe_request(request):
28+
headers["DJDT-STORE-ID"] = store_id
29+
return headers
30+
2331
@property
2432
def enabled(self):
2533
# Do not show the history panel if the panels are rendered on request
@@ -83,7 +91,9 @@ def content(self):
8391
for id, toolbar in reversed(self.toolbar._store.items()):
8492
stores[id] = {
8593
"toolbar": toolbar,
86-
"form": HistoryStoreForm(initial={"store_id": id}),
94+
"form": HistoryStoreForm(
95+
initial={"store_id": id, "exclude_history": True}
96+
),
8797
}
8898

8999
return render_to_string(
@@ -92,7 +102,10 @@ def content(self):
92102
"current_store_id": self.toolbar.store_id,
93103
"stores": stores,
94104
"refresh_form": HistoryStoreForm(
95-
initial={"store_id": self.toolbar.store_id}
105+
initial={
106+
"store_id": self.toolbar.store_id,
107+
"exclude_history": True,
108+
}
96109
),
97110
},
98111
)

debug_toolbar/panels/history/views.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ def history_sidebar(request):
1414
if form.is_valid():
1515
store_id = form.cleaned_data["store_id"]
1616
toolbar = DebugToolbar.fetch(store_id)
17+
exclude_history = form.cleaned_data["exclude_history"]
1718
context = {}
1819
if toolbar is None:
1920
# When the store_id has been popped already due to
2021
# RESULTS_CACHE_SIZE
2122
return JsonResponse(context)
2223
for panel in toolbar.panels:
23-
if not panel.is_historical:
24+
if exclude_history and not panel.is_historical:
2425
continue
2526
panel_context = {"panel": panel}
2627
context[panel.panel_id] = {
@@ -53,7 +54,12 @@ def history_refresh(request):
5354
"id": id,
5455
"store_context": {
5556
"toolbar": toolbar,
56-
"form": HistoryStoreForm(initial={"store_id": id}),
57+
"form": HistoryStoreForm(
58+
initial={
59+
"store_id": id,
60+
"exclude_history": True,
61+
}
62+
),
5763
},
5864
},
5965
),

debug_toolbar/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"SHOW_TEMPLATE_CONTEXT": True,
3838
"SKIP_TEMPLATE_PREFIXES": ("django/forms/widgets/", "admin/widgets/"),
3939
"SQL_WARNING_THRESHOLD": 500, # milliseconds
40+
"OBSERVE_REQUEST_CALLBACK": "debug_toolbar.toolbar.observe_request",
4041
}
4142

4243

debug_toolbar/static/debug_toolbar/js/history.js

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $$, ajaxForm } from "./utils.js";
1+
import { $$, ajaxForm, replaceToolbarState } from "./utils.js";
22

33
const djDebug = document.getElementById("djDebug");
44

@@ -12,9 +12,6 @@ function difference(setA, setB) {
1212

1313
/**
1414
* Create an array of dataset properties from a NodeList.
15-
* @param nodes
16-
* @param key
17-
* @returns {[]}
1815
*/
1916
function pluckData(nodes, key) {
2017
const data = [];
@@ -31,7 +28,7 @@ function refreshHistory() {
3128
pluckData(container.querySelectorAll("tr[data-store-id]"), "storeId")
3229
);
3330

34-
return ajaxForm(formTarget)
31+
ajaxForm(formTarget)
3532
.then(function (data) {
3633
// Remove existing rows first then re-populate with new data
3734
container
@@ -75,36 +72,32 @@ function refreshHistory() {
7572
});
7673
}
7774

78-
$$.on(djDebug, "click", ".switchHistory", function (event) {
79-
event.preventDefault();
80-
const newStoreId = this.dataset.storeId;
81-
const tbody = this.closest("tbody");
75+
function switchHistory(newStoreId) {
76+
const formTarget = djDebug.querySelector(
77+
".switchHistory[data-store-id='" + newStoreId + "']"
78+
);
79+
const tbody = formTarget.closest("tbody");
8280

8381
const highlighted = tbody.querySelector(".djdt-highlighted");
8482
if (highlighted) {
8583
highlighted.classList.remove("djdt-highlighted");
8684
}
87-
this.closest("tr").classList.add("djdt-highlighted");
85+
formTarget.closest("tr").classList.add("djdt-highlighted");
8886

89-
ajaxForm(this).then(function (data) {
90-
djDebug.setAttribute("data-store-id", newStoreId);
91-
// Check if response is empty, it could be due to an expired store_id.
87+
ajaxForm(formTarget).then(function (data) {
9288
if (Object.keys(data).length === 0) {
9389
const container = document.getElementById("djdtHistoryRequests");
9490
container.querySelector(
9591
'button[data-store-id="' + newStoreId + '"]'
9692
).innerHTML = "Switch [EXPIRED]";
97-
} else {
98-
Object.keys(data).forEach(function (panelId) {
99-
const panel = document.getElementById(panelId);
100-
if (panel) {
101-
panel.outerHTML = data[panelId].content;
102-
document.getElementById("djdt-" + panelId).outerHTML =
103-
data[panelId].button;
104-
}
105-
});
10693
}
94+
replaceToolbarState(newStoreId, data);
10795
});
96+
}
97+
98+
$$.on(djDebug, "click", ".switchHistory", function (event) {
99+
event.preventDefault();
100+
switchHistory(this.dataset.storeId);
108101
});
109102

110103
$$.on(djDebug, "click", ".refreshHistory", function (event) {

debug_toolbar/static/debug_toolbar/js/toolbar.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $$, ajax } from "./utils.js";
1+
import { $$, ajax, replaceToolbarState, debounce } from "./utils.js";
22

33
function onKeyDown(event) {
44
if (event.keyCode === 27) {
@@ -200,6 +200,9 @@ const djdt = {
200200
} else {
201201
djdt.hide_toolbar();
202202
}
203+
if (djDebug.dataset.sidebarUrl !== undefined) {
204+
djdt.update_on_ajax();
205+
}
203206
},
204207
hide_panels() {
205208
const djDebug = document.getElementById("djDebug");
@@ -253,6 +256,26 @@ const djdt = {
253256
localStorage.setItem("djdt.show", "true");
254257
window.removeEventListener("resize", djdt.ensure_handle_visibility);
255258
},
259+
update_on_ajax() {
260+
const sidebar_url =
261+
document.getElementById("djDebug").dataset.sidebarUrl;
262+
const slowjax = debounce(ajax, 200);
263+
264+
const origOpen = XMLHttpRequest.prototype.open;
265+
XMLHttpRequest.prototype.open = function () {
266+
this.addEventListener("load", function () {
267+
let store_id = this.getResponseHeader("djdt-store-id");
268+
if (store_id !== null) {
269+
store_id = encodeURIComponent(store_id);
270+
const dest = `${sidebar_url}?store_id=${store_id}`;
271+
slowjax(dest).then(function (data) {
272+
replaceToolbarState(store_id, data);
273+
});
274+
}
275+
});
276+
origOpen.apply(this, arguments);
277+
};
278+
},
256279
cookie: {
257280
get(key) {
258281
if (!document.cookie.includes(key)) {

debug_toolbar/static/debug_toolbar/js/utils.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,34 @@ function ajaxForm(element) {
104104
return ajax(url, ajaxData);
105105
}
106106

107-
export { $$, ajax, ajaxForm };
107+
function replaceToolbarState(newStoreId, data) {
108+
const djDebug = document.getElementById("djDebug");
109+
djDebug.setAttribute("data-store-id", newStoreId);
110+
// Check if response is empty, it could be due to an expired store_id.
111+
Object.keys(data).forEach(function (panelId) {
112+
const panel = document.getElementById(panelId);
113+
if (panel) {
114+
panel.outerHTML = data[panelId].content;
115+
document.getElementById("djdt-" + panelId).outerHTML =
116+
data[panelId].button;
117+
}
118+
});
119+
}
120+
121+
function debounce(func, delay) {
122+
let timer = null;
123+
let resolves = [];
124+
125+
return function (...args) {
126+
clearTimeout(timer);
127+
timer = setTimeout(() => {
128+
const result = func(...args);
129+
resolves.forEach((r) => r(result));
130+
resolves = [];
131+
}, delay);
132+
133+
return new Promise((r) => resolves.push(r));
134+
};
135+
}
136+
137+
export { $$, ajax, ajaxForm, replaceToolbarState, debounce };

debug_toolbar/templates/debug_toolbar/base.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
data-store-id="{{ toolbar.store_id }}"
1212
data-render-panel-url="{% url 'djdt:render_panel' %}"
1313
{% endif %}
14+
{% url 'djdt:history_sidebar' as history_url %}
15+
{% if history_url %}
16+
data-sidebar-url="{{ history_url }}"
17+
{% endif %}
1418
data-default-show="{% if toolbar.config.SHOW_COLLAPSED %}false{% else %}true{% endif %}"
1519
{{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }}>
1620
<div class="djdt-hidden" id="djDebugToolbar">

debug_toolbar/toolbar.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import uuid
66
from collections import OrderedDict
7+
from functools import lru_cache
78

89
from django.apps import apps
910
from django.core.exceptions import ImproperlyConfigured
@@ -130,7 +131,7 @@ def get_urls(cls):
130131
# Load URLs in a temporary variable for thread safety.
131132
# Global URLs
132133
urlpatterns = [
133-
path("render_panel/", views.render_panel, name="render_panel")
134+
path("render_panel/", views.render_panel, name="render_panel"),
134135
]
135136
# Per-panel URLs
136137
for panel_class in cls.get_panel_classes():
@@ -154,3 +155,21 @@ def is_toolbar_request(cls, request):
154155
except Resolver404:
155156
return False
156157
return resolver_match.namespaces and resolver_match.namespaces[-1] == app_name
158+
159+
@staticmethod
160+
@lru_cache(maxsize=128)
161+
def get_observe_request():
162+
# If OBSERVE_REQUEST_CALLBACK is a string, which is the recommended
163+
# setup, resolve it to the corresponding callable.
164+
func_or_path = dt_settings.get_config()["OBSERVE_REQUEST_CALLBACK"]
165+
if isinstance(func_or_path, str):
166+
return import_string(func_or_path)
167+
else:
168+
return func_or_path
169+
170+
171+
def observe_request(request):
172+
"""
173+
Determine whether to update the toolbar from a client side request.
174+
"""
175+
return not DebugToolbar.is_toolbar_request(request)

docs/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Next version
1313
``@override_settings``, to reconfigure the toolbar during tests.
1414
* Optimize rendering of SQL panel, saving about 30% of its run time.
1515
* New records in history panel will flash green.
16+
* Automatically update History panel on AJAX requests from client.
1617

1718
3.2.4 (2021-12-15)
1819
------------------

0 commit comments

Comments
 (0)