Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bb88c05
Add the Store API and initial documentation.
tim-schilling May 17, 2023
19b5695
Remove config from docs as sphinx says it's misspelled.
tim-schilling May 17, 2023
97fcda7
Switch to Store.request_ids and remove serialization force_str.
tim-schilling Jun 17, 2023
487dfb3
Log serialization warning when a panel errors. (#1810)
tim-schilling Aug 21, 2023
e7cf575
Ignore common venv folder.
tim-schilling Aug 20, 2023
c4201fa
Rename store_id variants to request_id
Aug 20, 2023
bbbbb34
Support serializable panels. This is a WIP and needs clean-up.
Aug 20, 2023
e2f695b
Support serializable sql panel
tim-schilling Aug 21, 2023
14a5e0c
Make Panel.panel_id a classmember.
tim-schilling Aug 21, 2023
a31115f
Force everything to a string if it can't be serialized.
tim-schilling Aug 29, 2023
71edcf5
Support serialization of FunctionCall
tim-schilling Aug 29, 2023
c03f08f
Update all panels to use data from get_stats on render
tim-schilling Sep 5, 2023
47bdabe
Extend example app to have an async version.
tim-schilling Sep 5, 2023
dd53424
Merge branch 'main' into serialize-panels
tim-schilling Jul 10, 2024
16e02f5
Rework the alerts panel to be compatible with serialization.
tim-schilling Jul 10, 2024
3e4c484
Make template panel serializable.
tim-schilling Jul 10, 2024
f4ff5f4
Avoid caching the config settings.
Jul 11, 2024
d3730a6
Fix tests for serializable changes with selenium.
Jul 11, 2024
c660269
Comment out the async button because it breaks the wsgi app.
Jul 11, 2024
8402c4d
Hack: Sleep before checking to see if the history panel auto updated.
Jul 11, 2024
e1c0755
Improve clarity of record_stats for serialization. (#1965)
tim-schilling Jul 19, 2024
5a21920
Merge branch 'main' into serializable
tim-schilling Feb 9, 2025
89c4786
Added check for pytest as test runner for IS_RUNNING_TESTS.
tim-schilling May 14, 2025
73eea66
Fixes #2073 -- Added DatabaseStore for persistent debug data storage.…
dr-rompecabezas May 14, 2025
153c22b
Merge branch 'main' into serializable
tim-schilling May 15, 2025
f8bfb0d
Move serializable changes into the main change log.
tim-schilling May 15, 2025
bf77c70
Updated replaceToolbarState to use request id.
tim-schilling May 15, 2025
0cbf478
Merge branch 'main' into serializable
matthiask May 23, 2025
7a638c7
Removed unnecessary SQLPanel.record_stats
tim-schilling Jun 3, 2025
2fafbb4
Merge branch 'main' into serializable
tim-schilling Jul 10, 2025
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ example_async:
--noinput --username="$(USER)" --email="$(USER)@mailinator.com"
daphne example.asgi:application

example_test:
example_test: ## Run the test suite for the example application
python example/manage.py test example

test: ## Run the test suite
Expand Down
3 changes: 2 additions & 1 deletion debug_toolbar/settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sys
import warnings
from functools import cache
Expand Down Expand Up @@ -44,7 +45,7 @@
"OBSERVE_REQUEST_CALLBACK": "debug_toolbar.toolbar.observe_request",
"TOOLBAR_LANGUAGE": None,
"TOOLBAR_STORE_CLASS": "debug_toolbar.store.MemoryStore",
"IS_RUNNING_TESTS": "test" in sys.argv,
"IS_RUNNING_TESTS": "test" in sys.argv or "PYTEST_VERSION" in os.environ,
"UPDATE_ON_FETCH": False,
}

Expand Down
26 changes: 13 additions & 13 deletions debug_toolbar/static/debug_toolbar/js/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ function refreshHistory() {
ajaxForm(formTarget)
.then((data) => {
// Remove existing rows first then re-populate with new data
container
.querySelectorAll("tr[data-request-id]")
.forEach(function (node) {
node.remove();
});
data.requests.forEach(function (request) {
for (const node of container.querySelectorAll(
"tr[data-request-id]"
)) {
node.remove();
}
for (const request of data.requests) {
container.innerHTML = request.content + container.innerHTML;
}
})
Expand All @@ -62,18 +62,18 @@ function refreshHistory() {
row.classList.add("flash-new");
}
setTimeout(() => {
container
.querySelectorAll("tr[data-request-id]")
.forEach((row) => {
row.classList.remove("flash-new");
});
for (const row of container.querySelectorAll(
"tr[data-request-id]"
)) {
row.classList.remove("flash-new");
}
}, 2000);
});
}

function switchHistory(newRequestId) {
const formTarget = djDebug.querySelector(
".switchHistory[data-request-id='" + newRequestId + "']"
`.switchHistory[data-request-id='${newRequestId}']`
);
const tbody = formTarget.closest("tbody");

Expand All @@ -87,7 +87,7 @@ function switchHistory(newRequestId) {
if (Object.keys(data).length === 0) {
const container = document.getElementById("djdtHistoryRequests");
container.querySelector(
'button[data-request-id="' + newRequestId + '"]'
`button[data-request-id="${newRequestId}"]`
).innerHTML = "Switch [EXPIRED]";
}
replaceToolbarState(newRequestId, data);
Expand Down
34 changes: 23 additions & 11 deletions debug_toolbar/static/debug_toolbar/js/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ const djdt = {
this.parentElement.classList.add("djdt-active");

const inner = current.querySelector(
".djDebugPanelContent .djdt-scroll"
),
requestId = djDebug.dataset.requestId;
".djDebugPanelContent .djdt-scroll"
);
const requestId = djDebug.dataset.requestId;
if (requestId && inner.children.length === 0) {
const url = new URL(
djDebug.dataset.renderPanelUrl,
Expand Down Expand Up @@ -297,11 +297,11 @@ const djdt = {
const slowjax = debounce(ajax, 200);

function handleAjaxResponse(requestId) {
requestId = encodeURIComponent(requestId);
const dest = `${sidebarUrl}?request_id=${requestId}`;
slowjax(dest).then(function (data) {
const encodedRequestId = encodeURIComponent(requestId);
const dest = `${sidebarUrl}?store_id=${encodedRequestId}`;
slowjax(dest).then((data) => {
if (djdt.needUpdateOnFetch) {
replaceToolbarState(requestId, data);
replaceToolbarState(encodedRequestId, data);
}
});
}
Expand All @@ -325,11 +325,23 @@ const djdt = {
};

const origFetch = window.fetch;
window.fetch = function () {
const promise = origFetch.apply(this, arguments);
promise.then(function (response) {
window.fetch = function (...args) {
// Heads up! Before modifying this code, please be aware of the
// possible unhandled errors that might arise from changing this.
// For details, see
// https://github.com/django-commons/django-debug-toolbar/pull/2100
const promise = origFetch.apply(this, args);
return promise.then((response) => {
if (response.headers.get("djdt-request-id") !== null) {
handleAjaxResponse(response.headers.get("djdt-request-id"));
try {
handleAjaxResponse(
response.headers.get("djdt-request-id")
);
} catch (err) {
throw new Error(
`"${err.name}" occurred within django-debug-toolbar: ${err.message}`
);
}
}
return response;
});
Expand Down
4 changes: 2 additions & 2 deletions debug_toolbar/static/debug_toolbar/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ function ajaxForm(element) {
function replaceToolbarState(newRequestId, data) {
const djDebug = document.getElementById("djDebug");
djDebug.setAttribute("data-request-id", newRequestId);
// Check if response is empty, it could be due to an expired requestId.
Object.keys(data).forEach(function (panelId) {
// Check if response is empty, it could be due to an expired storeId.
for (const panelId of Object.keys(data)) {
const panel = document.getElementById(panelId);
if (panel) {
panel.outerHTML = data[panelId].content;
Expand Down
3 changes: 3 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Serializable (don't include in main)
Pending
-------

* Added support for checking if pytest as the test runner when determining
if tests are running.

5.2.0 (2025-04-29)
------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Toolbar options

* ``IS_RUNNING_TESTS``

Default: ``"test" in sys.argv``
Default: ``"test" in sys.argv or "PYTEST_VERSION" in os.environ``

This setting whether the application is running tests. If this resolves to
``True``, the toolbar will prevent you from running tests. This should only
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ can do this by adding another setting:

.. code-block:: python

TESTING = "test" in sys.argv
TESTING = "test" in sys.argv or "PYTEST_VERSION" in os.environ

if not TESTING:
INSTALLED_APPS = [
Expand Down
2 changes: 1 addition & 1 deletion tests/panels/test_staticfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.shortcuts import render
from django.test import AsyncRequestFactory, RequestFactory

from debug_toolbar.panels.staticfiles import StaticFilesPanel
from debug_toolbar.panels.staticfiles import StaticFilesPanel, URLMixin

from ..base import BaseTestCase

Expand Down
96 changes: 63 additions & 33 deletions tests/test_csp_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,36 @@ def test_exists(self):
self._fail_on_invalid_html(content=response.content, parser=self.parser)
self.assertContains(response, "djDebug")

namespaces = get_namespaces(element=html_root)
nonce = response.context["request"].csp_nonce
self._fail_if_missing(
root=html_root, path=".//link", namespaces=namespaces, nonce=nonce
)
self._fail_if_missing(
root=html_root, path=".//script", namespaces=namespaces, nonce=nonce
)
namespaces = get_namespaces(element=html_root)
nonce = response.context["request"].csp_nonce
self._fail_if_missing(
root=html_root, path=".//link", namespaces=namespaces, nonce=nonce
)
self._fail_if_missing(
root=html_root, path=".//script", namespaces=namespaces, nonce=nonce
)

def test_does_not_exist_nonce_wasnt_used(self):
"""
A `nonce` should not exist even when using the `CSPMiddleware`
if the view didn't access the request.csp_nonce attribute.
"""
for middleware in [MIDDLEWARE_CSP_BEFORE, MIDDLEWARE_CSP_LAST]:
with self.settings(MIDDLEWARE=middleware):
response = cast(HttpResponse, self.client.get(path="/regular/basic/"))
self.assertEqual(response.status_code, 200)

html_root: Element = self.parser.parse(stream=response.content)
self._fail_on_invalid_html(content=response.content, parser=self.parser)
self.assertContains(response, "djDebug")

namespaces = get_namespaces(element=html_root)
self._fail_if_found(
root=html_root, path=".//link", namespaces=namespaces
)
self._fail_if_found(
root=html_root, path=".//script", namespaces=namespaces
)

@override_settings(
DEBUG_TOOLBAR_CONFIG={"DISABLE_PANELS": set()},
Expand All @@ -104,34 +126,42 @@ def test_redirects_exists(self):
self._fail_on_invalid_html(content=response.content, parser=self.parser)
self.assertContains(response, "djDebug")

namespaces = get_namespaces(element=html_root)
nonce = response.context["request"].csp_nonce
self._fail_if_missing(
root=html_root, path=".//link", namespaces=namespaces, nonce=nonce
)
self._fail_if_missing(
root=html_root, path=".//script", namespaces=namespaces, nonce=nonce
)
namespaces = get_namespaces(element=html_root)
nonce = response.context["request"].csp_nonce
self._fail_if_missing(
root=html_root, path=".//link", namespaces=namespaces, nonce=nonce
)
self._fail_if_missing(
root=html_root, path=".//script", namespaces=namespaces, nonce=nonce
)

def test_panel_content_nonce_exists(self):
store = get_store()
response = cast(HttpResponse, self.client.get(path="/regular/basic/"))
self.assertEqual(response.status_code, 200)
nonce = response.context["request"].csp_nonce

request_ids = list(store.request_ids())
toolbar = DebugToolbar.fetch(request_ids[0])
panels_to_check = ["HistoryPanel", "TimerPanel"]
for panel in panels_to_check:
content = toolbar.get_panel_by_id(panel).content
html_root: Element = self.parser.parse(stream=content)
namespaces = get_namespaces(element=html_root)
self._fail_if_missing(
root=html_root, path=".//link", namespaces=namespaces, nonce=nonce
)
self._fail_if_missing(
root=html_root, path=".//script", namespaces=namespaces, nonce=nonce
)
for middleware in [MIDDLEWARE_CSP_BEFORE, MIDDLEWARE_CSP_LAST]:
with self.settings(MIDDLEWARE=middleware):
response = cast(HttpResponse, self.client.get(path="/csp_view/"))
self.assertEqual(response.status_code, 200)

request_ids = list(store.request_ids())
toolbar = DebugToolbar.fetch(request_ids[-1])
panels_to_check = ["HistoryPanel", "TimerPanel"]
for panel in panels_to_check:
content = toolbar.get_panel_by_id(panel).content
html_root: Element = self.parser.parse(stream=content)
namespaces = get_namespaces(element=html_root)
nonce = str(toolbar.csp_nonce)
self._fail_if_missing(
root=html_root,
path=".//link",
namespaces=namespaces,
nonce=nonce,
)
self._fail_if_missing(
root=html_root,
path=".//script",
namespaces=namespaces,
nonce=nonce,
)

def test_missing(self):
"""A `nonce` should not exist when not using the `CSPMiddleware`."""
Expand Down
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ deps =
pygments
selenium>=4.8.0
sqlparse
django-csp<4.0
django-csp
django-template-partials
passenv=
CI
COVERAGE_ARGS
Expand Down