Skip to content

Adding Update on ajax feature #1577

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 11 additions & 20 deletions debug_toolbar/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@ def __call__(self, request):
panel.generate_stats(request, response)
panel.generate_server_timing(request, response)

response = self.generate_server_timing_header(response, toolbar.enabled_panels)

# Always render the toolbar for the history panel, even if it is not
# included in the response.
rendered = toolbar.render_toolbar()

for header, value in self.get_headers(request, toolbar.enabled_panels).items():
response.headers[header] = value

# Check for responses where the toolbar can't be inserted.
content_encoding = response.get("Content-Encoding", "")
content_type = response.get("Content-Type", "").split(";")[0]
Expand All @@ -96,22 +97,12 @@ def __call__(self, request):
return response

@staticmethod
def generate_server_timing_header(response, panels):
data = []

def get_headers(request, panels):
headers = {}
for panel in panels:
stats = panel.get_server_timing_stats()
if not stats:
continue

for key, record in stats.items():
# example: `SQLPanel_sql_time;dur=0;desc="SQL 0 queries"`
data.append(
'{}_{};dur={};desc="{}"'.format(
panel.panel_id, key, record.get("value"), record.get("title")
)
)

if data:
response["Server-Timing"] = ", ".join(data)
return response
for header, value in panel.get_headers(request).items():
if header in headers:
headers[header] += f", {value}"
else:
headers[header] = value
return headers
33 changes: 29 additions & 4 deletions debug_toolbar/panels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,41 @@ def process_request(self, request):
"""
return self.get_response(request)

def generate_stats(self, request, response):
def get_headers(self, request):
"""
Get headers the panel needs to set.

Called after :meth:`process_request
<debug_toolbar.panels.Panel.process_request>`, but may not be executed
on every request. This will only be called if the toolbar will be
inserted into the 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.
"""

Expand Down
1 change: 1 addition & 0 deletions debug_toolbar/panels/history/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ class HistoryStoreForm(forms.Form):
"""

store_id = forms.CharField(widget=forms.HiddenInput())
exclude_history = forms.BooleanField(widget=forms.HiddenInput(), required=False)
17 changes: 15 additions & 2 deletions debug_toolbar/panels/history/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ class HistoryPanel(Panel):
nav_title = _("History")
template = "debug_toolbar/panels/history.html"

def get_headers(self, request):
headers = super().get_headers(request)
observe_request = self.toolbar.get_observe_request()
store_id = getattr(self.toolbar, "store_id")
if store_id and observe_request(request):
headers["DJDT-STORE-ID"] = store_id
return headers

@property
def enabled(self):
# Do not show the history panel if the panels are rendered on request
Expand Down Expand Up @@ -83,7 +91,9 @@ def content(self):
for id, toolbar in reversed(self.toolbar._store.items()):
stores[id] = {
"toolbar": toolbar,
"form": HistoryStoreForm(initial={"store_id": id}),
"form": HistoryStoreForm(
initial={"store_id": id, "exclude_history": True}
),
}

return render_to_string(
Expand All @@ -92,7 +102,10 @@ def content(self):
"current_store_id": self.toolbar.store_id,
"stores": stores,
"refresh_form": HistoryStoreForm(
initial={"store_id": self.toolbar.store_id}
initial={
"store_id": self.toolbar.store_id,
"exclude_history": True,
}
),
},
)
Expand Down
10 changes: 8 additions & 2 deletions debug_toolbar/panels/history/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ def history_sidebar(request):
if form.is_valid():
store_id = form.cleaned_data["store_id"]
toolbar = DebugToolbar.fetch(store_id)
exclude_history = form.cleaned_data["exclude_history"]
context = {}
if toolbar is None:
# When the store_id has been popped already due to
# RESULTS_CACHE_SIZE
return JsonResponse(context)
for panel in toolbar.panels:
if not panel.is_historical:
if exclude_history and not panel.is_historical:
continue
panel_context = {"panel": panel}
context[panel.panel_id] = {
Expand Down Expand Up @@ -53,7 +54,12 @@ def history_refresh(request):
"id": id,
"store_context": {
"toolbar": toolbar,
"form": HistoryStoreForm(initial={"store_id": id}),
"form": HistoryStoreForm(
initial={
"store_id": id,
"exclude_history": True,
}
),
},
},
),
Expand Down
1 change: 1 addition & 0 deletions debug_toolbar/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"SHOW_TEMPLATE_CONTEXT": True,
"SKIP_TEMPLATE_PREFIXES": ("django/forms/widgets/", "admin/widgets/"),
"SQL_WARNING_THRESHOLD": 500, # milliseconds
"OBSERVE_REQUEST_CALLBACK": "debug_toolbar.toolbar.observe_request",
}


Expand Down
37 changes: 15 additions & 22 deletions debug_toolbar/static/debug_toolbar/js/history.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $$, ajaxForm } from "./utils.js";
import { $$, ajaxForm, replaceToolbarState } from "./utils.js";

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

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

/**
* Create an array of dataset properties from a NodeList.
* @param nodes
* @param key
* @returns {[]}
*/
function pluckData(nodes, key) {
const data = [];
Expand All @@ -31,7 +28,7 @@ function refreshHistory() {
pluckData(container.querySelectorAll("tr[data-store-id]"), "storeId")
);

return ajaxForm(formTarget)
ajaxForm(formTarget)
.then(function (data) {
// Remove existing rows first then re-populate with new data
container
Expand Down Expand Up @@ -75,36 +72,32 @@ function refreshHistory() {
});
}

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

const highlighted = tbody.querySelector(".djdt-highlighted");
if (highlighted) {
highlighted.classList.remove("djdt-highlighted");
}
this.closest("tr").classList.add("djdt-highlighted");
formTarget.closest("tr").classList.add("djdt-highlighted");

ajaxForm(this).then(function (data) {
djDebug.setAttribute("data-store-id", newStoreId);
// Check if response is empty, it could be due to an expired store_id.
ajaxForm(formTarget).then(function (data) {
if (Object.keys(data).length === 0) {
const container = document.getElementById("djdtHistoryRequests");
container.querySelector(
'button[data-store-id="' + newStoreId + '"]'
).innerHTML = "Switch [EXPIRED]";
} else {
Object.keys(data).forEach(function (panelId) {
const panel = document.getElementById(panelId);
if (panel) {
panel.outerHTML = data[panelId].content;
document.getElementById("djdt-" + panelId).outerHTML =
data[panelId].button;
}
});
}
replaceToolbarState(newStoreId, data);
});
}

$$.on(djDebug, "click", ".switchHistory", function (event) {
event.preventDefault();
switchHistory(this.dataset.storeId);
});

$$.on(djDebug, "click", ".refreshHistory", function (event) {
Expand Down
25 changes: 24 additions & 1 deletion debug_toolbar/static/debug_toolbar/js/toolbar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $$, ajax } from "./utils.js";
import { $$, ajax, replaceToolbarState, debounce } from "./utils.js";

function onKeyDown(event) {
if (event.keyCode === 27) {
Expand Down Expand Up @@ -200,6 +200,9 @@ const djdt = {
} else {
djdt.hide_toolbar();
}
if (djDebug.dataset.sidebarUrl !== undefined) {
djdt.update_on_ajax();
}
},
hide_panels() {
const djDebug = document.getElementById("djDebug");
Expand Down Expand Up @@ -253,6 +256,26 @@ const djdt = {
localStorage.setItem("djdt.show", "true");
window.removeEventListener("resize", djdt.ensure_handle_visibility);
},
update_on_ajax() {
const sidebar_url =
document.getElementById("djDebug").dataset.sidebarUrl;
const slowjax = debounce(ajax, 200);

const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {
this.addEventListener("load", function () {
let store_id = this.getResponseHeader("djdt-store-id");
if (store_id !== null) {
store_id = encodeURIComponent(store_id);
const dest = `${sidebar_url}?store_id=${store_id}`;
slowjax(dest).then(function (data) {
replaceToolbarState(store_id, data);
});
}
});
origOpen.apply(this, arguments);
};
},
cookie: {
get(key) {
if (!document.cookie.includes(key)) {
Expand Down
32 changes: 31 additions & 1 deletion debug_toolbar/static/debug_toolbar/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,34 @@ function ajaxForm(element) {
return ajax(url, ajaxData);
}

export { $$, ajax, ajaxForm };
function replaceToolbarState(newStoreId, data) {
const djDebug = document.getElementById("djDebug");
djDebug.setAttribute("data-store-id", newStoreId);
// Check if response is empty, it could be due to an expired store_id.
Object.keys(data).forEach(function (panelId) {
const panel = document.getElementById(panelId);
if (panel) {
panel.outerHTML = data[panelId].content;
document.getElementById("djdt-" + panelId).outerHTML =
data[panelId].button;
}
});
}

function debounce(func, delay) {
let timer = null;
let resolves = [];

return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
const result = func(...args);
resolves.forEach((r) => r(result));
resolves = [];
}, delay);

return new Promise((r) => resolves.push(r));
};
}

export { $$, ajax, ajaxForm, replaceToolbarState, debounce };
4 changes: 4 additions & 0 deletions debug_toolbar/templates/debug_toolbar/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
data-store-id="{{ toolbar.store_id }}"
data-render-panel-url="{% url 'djdt:render_panel' %}"
{% endif %}
{% url 'djdt:history_sidebar' as history_url %}
{% if history_url %}
data-sidebar-url="{{ history_url }}"
{% endif %}
data-default-show="{% if toolbar.config.SHOW_COLLAPSED %}false{% else %}true{% endif %}"
{{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }}>
<div class="djdt-hidden" id="djDebugToolbar">
Expand Down
21 changes: 20 additions & 1 deletion debug_toolbar/toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import uuid
from collections import OrderedDict
from functools import lru_cache

from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -130,7 +131,7 @@ def get_urls(cls):
# Load URLs in a temporary variable for thread safety.
# Global URLs
urlpatterns = [
path("render_panel/", views.render_panel, name="render_panel")
path("render_panel/", views.render_panel, name="render_panel"),
]
# Per-panel URLs
for panel_class in cls.get_panel_classes():
Expand All @@ -154,3 +155,21 @@ def is_toolbar_request(cls, request):
except Resolver404:
return False
return resolver_match.namespaces and resolver_match.namespaces[-1] == app_name

@staticmethod
@lru_cache(maxsize=128)
def get_observe_request():
# If OBSERVE_REQUEST_CALLBACK is a string, which is the recommended
# setup, resolve it to the corresponding callable.
func_or_path = dt_settings.get_config()["OBSERVE_REQUEST_CALLBACK"]
if isinstance(func_or_path, str):
return import_string(func_or_path)
else:
return func_or_path


def observe_request(request):
"""
Determine whether to update the toolbar from a client side request.
"""
return not DebugToolbar.is_toolbar_request(request)
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Next version
``@override_settings``, to reconfigure the toolbar during tests.
* Optimize rendering of SQL panel, saving about 30% of its run time.
* New records in history panel will flash green.
* Automatically update History panel on AJAX requests from client.

3.2.4 (2021-12-15)
------------------
Expand Down
Loading