From b041e7cbe4c19f87d96edd7804ab1fd042826f9f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:30:40 +0000 Subject: [PATCH 001/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-eslint: v8.56.0 → v9.0.0-alpha.2](https://github.com/pre-commit/mirrors-eslint/compare/v8.56.0...v9.0.0-alpha.2) - [github.com/astral-sh/ruff-pre-commit: v0.1.11 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.11...v0.2.0) - [github.com/tox-dev/pyproject-fmt: 1.5.3 → 1.7.0](https://github.com/tox-dev/pyproject-fmt/compare/1.5.3...1.7.0) - [github.com/abravalheri/validate-pyproject: v0.15 → v0.16](https://github.com/abravalheri/validate-pyproject/compare/v0.15...v0.16) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2f93ac73..f2dea2dd0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,16 +40,16 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.1.11' + rev: 'v0.2.0' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.5.3 + rev: 1.7.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.15 + rev: v0.16 hooks: - id: validate-pyproject From 6591d021c8c363d26d4d6ae6d9afa59181443945 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Mon, 5 Feb 2024 19:08:13 +0100 Subject: [PATCH 002/200] Make ruff complain less --- debug_toolbar/panels/profiling.py | 6 ++---- pyproject.toml | 14 ++++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index 2b7742400..64224a2db 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -86,12 +86,10 @@ def func_std_string(self): # match what old profile produced ) def subfuncs(self): - i = 0 h, s, v = self.hsv count = len(self.statobj.all_callees[self.func]) - for func, stats in self.statobj.all_callees[self.func].items(): - i += 1 - h1 = h + (i / count) / (self.depth + 1) + for i, (func, stats) in enumerate(self.statobj.all_callees[self.func].items()): + h1 = h + ((i + 1) / count) / (self.depth + 1) s1 = 0 if stats[3] == 0 else s * (stats[3] / self.stats[3]) yield FunctionCall( self.statobj, diff --git a/pyproject.toml b/pyproject.toml index e529808cb..944543d91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,11 @@ packages = ["debug_toolbar"] path = "debug_toolbar/__init__.py" [tool.ruff] +fix = true +show-fixes = true +target-version = "py38" + +[tool.ruff.lint] extend-select = [ "ASYNC", # flake8-async "B", # flake8-bugbear @@ -75,17 +80,14 @@ extend-ignore = [ "E501", # Ignore line length violations "SIM108", # Use ternary operator instead of if-else-block ] -fix = true -show-fixes = true -target-version = "py38" -[tool.ruff.isort] +[tool.ruff.lint.isort] combine-as-imports = true -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 16 -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "*/migrat*/*" = [ "N806", # Allow using PascalCase model names in migrations "N999", # Ignore the fact that migration files are invalid module names From 757b82e4c71676ecd9db5aa7b0ab3206644eb37c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:51:09 +0100 Subject: [PATCH 003/200] [pre-commit.ci] pre-commit autoupdate (#1867) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Matthias Kestenholz Co-authored-by: Christian Clauss --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2dea2dd0..23d879f71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.2.0' + rev: 'v0.2.1' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From c688ce4ad7d18c5ecb800869298ca8cf6c08be1d Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Sun, 11 Feb 2024 09:55:32 -0600 Subject: [PATCH 004/200] Use url template tag for example URLs (#1879) --- example/templates/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/templates/index.html b/example/templates/index.html index 382bfb0e9..ee00d5f05 100644 --- a/example/templates/index.html +++ b/example/templates/index.html @@ -12,8 +12,8 @@

Index of Tests

  • jQuery 3.3.1
  • MooTools 1.6.0
  • Prototype 1.7.3.0
  • -
  • Hotwire Turbo
  • -
  • htmx
  • +
  • Hotwire Turbo
  • +
  • htmx
  • Django Admin

    {% endcache %} From 64697a4cdc7987a31bbc41d8f6802627f64c58c4 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 13 Feb 2024 13:56:49 +0100 Subject: [PATCH 005/200] Keep GitHub Actions up to date with GitHub's Dependabot (#1876) Autogenerates pull requests like #1660 * https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot * https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..be006de9a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly From 14fbaa83b698d02a9bdb5b55575288f45015df4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:35:56 +0100 Subject: [PATCH 006/200] Bump the github-actions group with 4 updates (#1885) * Bump the github-actions group with 4 updates Bumps the github-actions group with 4 updates: [actions/setup-python](https://github.com/actions/setup-python), [actions/cache](https://github.com/actions/cache), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/setup-python` from 4 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) Updates `actions/cache` from 3 to 4 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) Updates `actions/upload-artifact` from 3 to 4 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) Updates `actions/download-artifact` from 3 to 4 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] * Attempt fixing the upload-artifact name collisions --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matthias Kestenholz --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 37 ++++++++++++++++++----------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2059d37f8..b57181444 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb28e217e..72b40d010 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -44,7 +44,7 @@ jobs: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: @@ -67,9 +67,9 @@ jobs: DB_PORT: 3306 - name: Upload coverage data - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage-data + name: coverage-data-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.arch }}-mysql path: ".coverage.*" postgres: @@ -108,7 +108,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -119,7 +119,7 @@ jobs: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: @@ -145,9 +145,9 @@ jobs: DB_PORT: 5432 - name: Upload coverage data - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage-data + name: coverage-data-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.database }} path: ".coverage.*" sqlite: @@ -162,7 +162,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -173,7 +173,7 @@ jobs: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: @@ -193,9 +193,9 @@ jobs: DB_NAME: ":memory:" - name: Upload coverage data - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage-data + name: coverage-data-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.arch }}-sqlite path: ".coverage.*" coverage: @@ -204,7 +204,7 @@ jobs: needs: [sqlite, mysql, postgres] steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: # Use latest, so it understands all syntax. python-version: "3.11" @@ -212,9 +212,10 @@ jobs: - run: python -m pip install --upgrade coverage[toml] - name: Download coverage data. - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: coverage-data + pattern: coverage-data-* + merge-multiple: true - name: Combine coverage & check percentage run: | @@ -223,7 +224,7 @@ jobs: python -m coverage report - name: Upload HTML report if check failed. - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: html-report path: htmlcov @@ -238,7 +239,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -248,7 +249,7 @@ jobs: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: From 38d2eea5f8b57f70a4b5bdebd6bdeae8c395d822 Mon Sep 17 00:00:00 2001 From: Elijah Okello Date: Wed, 14 Feb 2024 11:37:21 +0300 Subject: [PATCH 007/200] #1870 fix pre commit errors (#1884) * fix: fix for precommit errors * fix: migrated .eslintrc.js to eslint.config.js flat config * fix: added dependencies and added support for globals in eslint.config.js * fix: set ecmaVersion to latest --- .eslintrc.js | 22 ---------------------- .pre-commit-config.yaml | 8 ++++++-- eslint.config.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 24 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 eslint.config.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b0c799d88..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - root: true, - "env": { - "browser": true, - "es6": true, - node: true, - }, - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "rules": { - "curly": ["error", "all"], - "dot-notation": "error", - "eqeqeq": "error", - "no-eval": "error", - "no-var": "error", - "prefer-const": "error", - "semi": "error" - } -}; diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23d879f71..116a58560 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/adamchainz/django-upgrade - rev: 1.15.0 + rev: 1.16.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] @@ -32,9 +32,13 @@ repos: args: - --trailing-comma=es5 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.56.0 + rev: v9.0.0-beta.0 hooks: - id: eslint + additional_dependencies: + - "eslint@v9.0.0-beta.0" + - "@eslint/js@v9.0.0-beta.0" + - "globals" files: \.js?$ types: [file] args: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..0b4d0e49e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +const js = require("@eslint/js"); +const globals = require("globals"); + +module.exports = [ + js.configs.recommended, + { + files: ["**/*.js"], + languageOptions:{ + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + rules: { + "curly": ["error", "all"], + "dot-notation": "error", + "eqeqeq": "error", + "no-eval": "error", + "no-var": "error", + "prefer-const": "error", + "semi": "error" + } + } +]; From 0663276eb9e1dbfaf993733aef2005de20faa951 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:42:14 +0100 Subject: [PATCH 008/200] [pre-commit.ci] pre-commit autoupdate (#1888) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 116a58560..3876810d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.2.1' + rev: 'v0.2.2' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 7d77a34dcd00bcbc1561541877be132bef1789a5 Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Thu, 22 Feb 2024 13:42:50 -0600 Subject: [PATCH 009/200] Show toolbar for docker's internal IP address (#1887) Fixes #1854 --- debug_toolbar/middleware.py | 18 +++++++++++++++++- docs/changes.rst | 3 +++ docs/installation.rst | 10 ++++------ tests/test_integration.py | 9 +++++++++ 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py index b5e5d0827..38cf92884 100644 --- a/debug_toolbar/middleware.py +++ b/debug_toolbar/middleware.py @@ -3,6 +3,7 @@ """ import re +import socket from functools import lru_cache from django.conf import settings @@ -19,7 +20,22 @@ def show_toolbar(request): """ Default function to determine whether to show the toolbar on a given page. """ - return settings.DEBUG and request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS + internal_ips = settings.INTERNAL_IPS.copy() + + try: + # This is a hack for docker installations. It attempts to look + # up the IP address of the docker host. + # This is not guaranteed to work. + docker_ip = ( + # Convert the last segment of the IP address to be .1 + ".".join(socket.gethostbyname("host.docker.internal").rsplit(".")[:-1]) + + ".1" + ) + internal_ips.append(docker_ip) + except socket.gaierror: + # It's fine if the lookup errored since they may not be using docker + pass + return settings.DEBUG and request.META.get("REMOTE_ADDR") in internal_ips @lru_cache(maxsize=None) diff --git a/docs/changes.rst b/docs/changes.rst index e2a610991..3caa35419 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,9 @@ Change log Pending ------- +* Automatically support Docker rather than having the developer write a + workaround for ``INTERNAL_IPS``. + 4.3.0 (2024-02-01) ------------------ diff --git a/docs/installation.rst b/docs/installation.rst index a350d9c3a..1f2e1f119 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -145,12 +145,10 @@ option. .. warning:: - If using Docker the following will set your ``INTERNAL_IPS`` correctly in Debug mode:: - - if DEBUG: - import socket # only if you haven't already imported this - hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) - INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + ["127.0.0.1", "10.0.2.2"] + If using Docker, the toolbar will attempt to look up your host name + automatically and treat it as an allowable internal IP. If you're not + able to get the toolbar to work with your docker installation, review + the code in ``debug_toolbar.middleware.show_toolbar``. Troubleshooting --------------- diff --git a/tests/test_integration.py b/tests/test_integration.py index 379fafaf4..4cfc84a78 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -2,6 +2,7 @@ import re import time import unittest +from unittest.mock import patch import html5lib from django.contrib.staticfiles.testing import StaticLiveServerTestCase @@ -66,6 +67,14 @@ def test_show_toolbar_INTERNAL_IPS(self): with self.settings(INTERNAL_IPS=[]): self.assertFalse(show_toolbar(self.request)) + @patch("socket.gethostbyname", return_value="127.0.0.255") + def test_show_toolbar_docker(self, mocked_gethostbyname): + with self.settings(INTERNAL_IPS=[]): + # Is true because REMOTE_ADDR is 127.0.0.1 and the 255 + # is shifted to be 1. + self.assertTrue(show_toolbar(self.request)) + mocked_gethostbyname.assert_called_once_with("host.docker.internal") + def test_should_render_panels_RENDER_PANELS(self): """ The toolbar should force rendering panels on each request From e80c05df3425554a2cae1db064fc07e445b61a79 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Thu, 22 Feb 2024 20:43:21 +0100 Subject: [PATCH 010/200] Raise the minimum Django version to 4.2 (#1880) --- .pre-commit-config.yaml | 2 +- README.rst | 4 ++-- docs/changes.rst | 1 + pyproject.toml | 4 +--- tests/test_integration.py | 36 ++++++++++++++++++++++++------------ tox.ini | 16 ++++++---------- 6 files changed, 35 insertions(+), 28 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3876810d1..608371cea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: rev: 1.16.0 hooks: - id: django-upgrade - args: [--target-version, "3.2"] + args: [--target-version, "4.2"] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/README.rst b/README.rst index 0eaaa6bd3..2408e1b83 100644 --- a/README.rst +++ b/README.rst @@ -44,8 +44,8 @@ Here's a screenshot of the toolbar in action: In addition to the built-in panels, a number of third-party panels are contributed by the community. -The current stable version of the Debug Toolbar is 4.1.0. It works on -Django ≥ 3.2.4. +The current stable version of the Debug Toolbar is 4.3.0. It works on +Django ≥ 4.2.0. The Debug Toolbar does not currently support `Django's asynchronous views `_. diff --git a/docs/changes.rst b/docs/changes.rst index 3caa35419..a3bf6a934 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,7 @@ Change log Pending ------- +* Raised the minimum Django version to 4.2. * Automatically support Docker rather than having the developer write a workaround for ``INTERNAL_IPS``. diff --git a/pyproject.toml b/pyproject.toml index 944543d91..5e8a47516 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,8 +17,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 3.2", - "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Intended Audience :: Developers", @@ -37,7 +35,7 @@ dynamic = [ "version", ] dependencies = [ - "Django>=3.2.4", + "Django>=4.2.9", "sqlparse>=0.2", ] [project.urls] diff --git a/tests/test_integration.py b/tests/test_integration.py index 4cfc84a78..fee67b7d1 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -266,13 +266,15 @@ def test_render_panel_checks_show_toolbar(self): response = self.client.get(url, data) self.assertEqual(response.status_code, 200) - response = self.client.get(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + response = self.client.get( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) self.assertEqual(response.status_code, 200) with self.settings(INTERNAL_IPS=[]): response = self.client.get(url, data) self.assertEqual(response.status_code, 404) response = self.client.get( - url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, data, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 404) @@ -302,13 +304,15 @@ def test_template_source_checks_show_toolbar(self): response = self.client.get(url, data) self.assertEqual(response.status_code, 200) - response = self.client.get(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + response = self.client.get( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) self.assertEqual(response.status_code, 200) with self.settings(INTERNAL_IPS=[]): response = self.client.get(url, data) self.assertEqual(response.status_code, 404) response = self.client.get( - url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, data, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 404) @@ -348,13 +352,15 @@ def test_sql_select_checks_show_toolbar(self): response = self.client.post(url, data) self.assertEqual(response.status_code, 200) - response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) self.assertEqual(response.status_code, 200) with self.settings(INTERNAL_IPS=[]): response = self.client.post(url, data) self.assertEqual(response.status_code, 404) response = self.client.post( - url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, data, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 404) @@ -374,13 +380,15 @@ def test_sql_explain_checks_show_toolbar(self): response = self.client.post(url, data) self.assertEqual(response.status_code, 200) - response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) self.assertEqual(response.status_code, 200) with self.settings(INTERNAL_IPS=[]): response = self.client.post(url, data) self.assertEqual(response.status_code, 404) response = self.client.post( - url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, data, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 404) @@ -406,13 +414,15 @@ def test_sql_explain_postgres_json_field(self): } response = self.client.post(url, data) self.assertEqual(response.status_code, 200) - response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) self.assertEqual(response.status_code, 200) with self.settings(INTERNAL_IPS=[]): response = self.client.post(url, data) self.assertEqual(response.status_code, 404) response = self.client.post( - url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, data, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 404) @@ -432,13 +442,15 @@ def test_sql_profile_checks_show_toolbar(self): response = self.client.post(url, data) self.assertEqual(response.status_code, 200) - response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) self.assertEqual(response.status_code, 200) with self.settings(INTERNAL_IPS=[]): response = self.client.post(url, data) self.assertEqual(response.status_code, 404) response = self.client.post( - url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, data, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 404) diff --git a/tox.ini b/tox.ini index 254fb9b76..67436888f 100644 --- a/tox.ini +++ b/tox.ini @@ -3,16 +3,13 @@ isolated_build = true envlist = docs packaging - py{38,39,310}-dj32-{sqlite,postgresql,postgis,mysql} - py{310,311}-dj41-{sqlite,postgresql,postgis,mysql} + py{38,39,310,311,312}-dj{42}-{sqlite,postgresql,postgis,mysql} py{310,311,312}-dj{42,50,main}-{sqlite,postgresql,psycopg3,postgis,mysql} [testenv] deps = - dj32: django~=3.2.9 - dj41: django~=4.1.3 dj42: django~=4.2.1 - dj50: django~=5.0a1 + dj50: django~=5.0.2 djmain: https://github.com/django/django/archive/main.tar.gz postgresql: psycopg2-binary psycopg3: psycopg[binary] @@ -37,7 +34,6 @@ passenv= setenv = PYTHONPATH = {toxinidir} PYTHONWARNINGS = d - py39-dj32-postgresql: DJANGO_SELENIUM_TESTS = true py311-dj42-postgresql: DJANGO_SELENIUM_TESTS = true DB_NAME = {env:DB_NAME:debug_toolbar} DB_USER = {env:DB_USER:debug_toolbar} @@ -49,28 +45,28 @@ pip_pre = True commands = python -b -W always -m coverage run -m django test -v2 {posargs:tests} -[testenv:py{38,39,310,311,312}-dj{32,41,42,50,main}-{postgresql,psycopg3}] +[testenv:py{38,39,310,311,312}-dj{42,50,main}-{postgresql,psycopg3}] setenv = {[testenv]setenv} DB_BACKEND = postgresql DB_PORT = {env:DB_PORT:5432} -[testenv:py{38,39,310,311,312}-dj{32,41,42,50,main}-postgis] +[testenv:py{38,39,310,311,312}-dj{42,50,main}-postgis] setenv = {[testenv]setenv} DB_BACKEND = postgis DB_PORT = {env:DB_PORT:5432} -[testenv:py{38,39,310,311,312}-dj{32,41,42,50,main}-mysql] +[testenv:py{38,39,310,311,312}-dj{42,50,main}-mysql] setenv = {[testenv]setenv} DB_BACKEND = mysql DB_PORT = {env:DB_PORT:3306} -[testenv:py{38,39,310,311,312}-dj{32,41,42,50,main}-sqlite] +[testenv:py{38,39,310,311,312}-dj{42,50,main}-sqlite] setenv = {[testenv]setenv} DB_BACKEND = sqlite3 From 9f66bd3c2f3d155897f47f19786b8ed7e78ed5ea Mon Sep 17 00:00:00 2001 From: Elineda Date: Thu, 22 Feb 2024 20:54:19 +0100 Subject: [PATCH 011/200] Improve handling when djdt views dont respond with JSON (#1877) --- debug_toolbar/static/debug_toolbar/js/utils.js | 6 +++++- docs/changes.rst | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/debug_toolbar/static/debug_toolbar/js/utils.js b/debug_toolbar/static/debug_toolbar/js/utils.js index b4c7a4cb8..c37525f13 100644 --- a/debug_toolbar/static/debug_toolbar/js/utils.js +++ b/debug_toolbar/static/debug_toolbar/js/utils.js @@ -75,7 +75,11 @@ function ajax(url, init) { return fetch(url, init) .then(function (response) { if (response.ok) { - return response.json(); + return response.json().catch(function(error){ + return Promise.reject( + new Error("The response is a invalid Json object : " + error) + ); + }); } return Promise.reject( new Error(response.status + ": " + response.statusText) diff --git a/docs/changes.rst b/docs/changes.rst index a3bf6a934..3ffa31dea 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -7,6 +7,8 @@ Pending * Raised the minimum Django version to 4.2. * Automatically support Docker rather than having the developer write a workaround for ``INTERNAL_IPS``. +* Display a better error message when the toolbar's requests + return invalid json. 4.3.0 (2024-02-01) ------------------ From b9e4af7518d8f61a82055e6cea0900d0ce938411 Mon Sep 17 00:00:00 2001 From: Pascal Fouque Date: Thu, 22 Feb 2024 21:42:50 +0100 Subject: [PATCH 012/200] Fix DeprecationWarnings about form default templates (#1878) Co-authored-by: tschilling --- debug_toolbar/templates/debug_toolbar/panels/history.html | 2 +- debug_toolbar/templates/debug_toolbar/panels/history_tr.html | 2 +- debug_toolbar/templates/debug_toolbar/panels/sql.html | 2 +- docs/changes.rst | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/debug_toolbar/templates/debug_toolbar/panels/history.html b/debug_toolbar/templates/debug_toolbar/panels/history.html index 84c6cb5bd..840f6c9f4 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/history.html +++ b/debug_toolbar/templates/debug_toolbar/panels/history.html @@ -1,6 +1,6 @@ {% load i18n %}{% load static %}
    - {{ refresh_form }} + {{ refresh_form.as_div }}
    diff --git a/debug_toolbar/templates/debug_toolbar/panels/history_tr.html b/debug_toolbar/templates/debug_toolbar/panels/history_tr.html index 31793472a..eff544f1a 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/history_tr.html +++ b/debug_toolbar/templates/debug_toolbar/panels/history_tr.html @@ -43,7 +43,7 @@ diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql.html b/debug_toolbar/templates/debug_toolbar/panels/sql.html index 6080e9f19..e5bf0b7f6 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql.html @@ -77,7 +77,7 @@ {% if query.params %} {% if query.is_select %} - {{ query.form }} + {{ query.form.as_div }} {% if query.vendor == 'mysql' %} diff --git a/docs/changes.rst b/docs/changes.rst index 3ffa31dea..940a706ef 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -9,6 +9,7 @@ Pending workaround for ``INTERNAL_IPS``. * Display a better error message when the toolbar's requests return invalid json. +* Render forms with ``as_div`` to silence Django 5.0 deprecation warnings. 4.3.0 (2024-02-01) ------------------ From eb0b6ea2211c365728e32d086e475defaaec5a0f Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Tue, 5 Mar 2024 12:44:52 +0100 Subject: [PATCH 013/200] Update pre-commit hooks --- .pre-commit-config.yaml | 8 ++++---- debug_toolbar/panels/templates/panel.py | 6 +++--- docs/changes.rst | 1 + tests/sync.py | 1 + tests/urls_invalid.py | 1 + tests/urls_use_package_urls.py | 1 + 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 608371cea..25efec746 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,19 +32,19 @@ repos: args: - --trailing-comma=es5 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.0.0-beta.0 + rev: v9.0.0-beta.1 hooks: - id: eslint additional_dependencies: - - "eslint@v9.0.0-beta.0" - - "@eslint/js@v9.0.0-beta.0" + - "eslint@v9.0.0-beta.1" + - "@eslint/js@v9.0.0-beta.1" - "globals" files: \.js?$ types: [file] args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.2.2' + rev: 'v0.3.0' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index f8c9242ca..c0c6246b2 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -154,9 +154,9 @@ def process_context_list(self, context_layers): # QuerySet would trigger the database: user can run the # query from SQL Panel elif isinstance(value, (QuerySet, RawQuerySet)): - temp_layer[ - key - ] = f"<<{value.__class__.__name__.lower()} of {value.model._meta.label}>>" + temp_layer[key] = ( + f"<<{value.__class__.__name__.lower()} of {value.model._meta.label}>>" + ) else: token = allow_sql.set(False) # noqa: FBT003 try: diff --git a/docs/changes.rst b/docs/changes.rst index 940a706ef..7e166d3b2 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,6 +10,7 @@ Pending * Display a better error message when the toolbar's requests return invalid json. * Render forms with ``as_div`` to silence Django 5.0 deprecation warnings. +* Stayed on top of pre-commit hook updates. 4.3.0 (2024-02-01) ------------------ diff --git a/tests/sync.py b/tests/sync.py index d71298089..d7a9872fd 100644 --- a/tests/sync.py +++ b/tests/sync.py @@ -1,6 +1,7 @@ """ Taken from channels.db """ + from asgiref.sync import SyncToAsync from django.db import close_old_connections diff --git a/tests/urls_invalid.py b/tests/urls_invalid.py index ccadb6735..a2a56699c 100644 --- a/tests/urls_invalid.py +++ b/tests/urls_invalid.py @@ -1,2 +1,3 @@ """Invalid urls.py file for testing""" + urlpatterns = [] diff --git a/tests/urls_use_package_urls.py b/tests/urls_use_package_urls.py index 50f7dfd69..0a3a91ab3 100644 --- a/tests/urls_use_package_urls.py +++ b/tests/urls_use_package_urls.py @@ -1,4 +1,5 @@ """urls.py to test using debug_toolbar.urls in include""" + from django.urls import include, path import debug_toolbar From 0baef8ccc0a1b8719a3176bd3b23930c9c660b3c Mon Sep 17 00:00:00 2001 From: tschilling Date: Sun, 3 Mar 2024 11:50:57 -0600 Subject: [PATCH 014/200] Add architecture documentation for the project. This starts with high level information about the project. In the future we can go into more detail on each of the components. --- docs/architecture.rst | 84 +++++++++++++++++++++++++++++++++++++++++++ docs/changes.rst | 2 ++ docs/contributing.rst | 6 ++++ docs/index.rst | 1 + 4 files changed, 93 insertions(+) create mode 100644 docs/architecture.rst diff --git a/docs/architecture.rst b/docs/architecture.rst new file mode 100644 index 000000000..7be5ac78d --- /dev/null +++ b/docs/architecture.rst @@ -0,0 +1,84 @@ +Architecture +============ + +The Django Debug Toolbar is designed to be flexible and extensible for +developers and third-party panel creators. + +Core Components +--------------- + +While there are several components, the majority of logic and complexity +lives within the following: + +- ``debug_toolbar.middleware.DebugToolbarMiddleware`` +- ``debug_toolbar.toolbar.DebugToolbar`` +- ``debug_toolbar.panels`` + +^^^^^^^^^^^^^^^^^^^^^^ +DebugToolbarMiddleware +^^^^^^^^^^^^^^^^^^^^^^ + +The middleware is how the toolbar integrates with Django projects. +It determines if the toolbar should instrument the request, which +panels to use, facilitates the processing of the request and augmenting +the response with the toolbar. Most logic for how the toolbar interacts +with the user's Django project belongs here. + +^^^^^^^^^^^^ +DebugToolbar +^^^^^^^^^^^^ + +The ``DebugToolbar`` class orchestrates the processing of a request +for each of the panels. It contains the logic that needs to be aware +of all the panels, but doesn't need to interact with the user's Django +project. + +^^^^^^ +Panels +^^^^^^ + +The majority of the complex logic lives within the panels themselves. This +is because the panels are responsible for collecting the various metrics. +Some of the metrics are collected via +`monkey-patching `_, such as +``TemplatesPanel``. Others, such as ``SettingsPanel`` don't need to collect +anything and include the data directly in the response. + +Some panels such as ``SQLPanel`` have additional functionality. This tends +to involve a user clicking on something, and the toolbar presenting a new +page with additional data. That additional data is handled in views defined +in the panels package (for example, ``debug_toolbar.panels.sql.views``). + +Logic Flow +---------- + +When a request comes in, the toolbar first interacts with it in the +middleware. If the middleware determines the request should be instrumented, +it will instantiate the toolbar and pass the request for processing. The +toolbar will use the enabled panels to collect information on the request +and/or response. When the toolbar has completed collecting its metrics on +both the request and response, the middleware will collect the results +from the toolbar. It will inject the HTML and JavaScript to render the +toolbar as well as any headers into the response. + +After the browser renders the panel and the user interacts with it, the +toolbar's JavaScript will send requests to the server. If the view handling +the request needs to fetch data from the toolbar, the request must supply +the store ID. This is so that the toolbar can load the collected metrics +for that particular request. + +The history panel allows a user to view the metrics for any request since +the application was started. The toolbar maintains its state entirely in +memory for the process running ``runserver``. If the application is +restarted the toolbar will lose its state. + +Problematic Parts +----------------- + +- ``debug.panels.templates.panel``: This monkey-patches template rendering + when the panel module is loaded +- ``debug.panels.sql``: This package is particularly complex, but provides + the main benefit of the toolbar +- Support for async and multi-threading: This is currently unsupported, but + is being implemented as per the + `Async compatible toolbar project `_. diff --git a/docs/changes.rst b/docs/changes.rst index 7e166d3b2..fd79230af 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -11,6 +11,8 @@ Pending return invalid json. * Render forms with ``as_div`` to silence Django 5.0 deprecation warnings. * Stayed on top of pre-commit hook updates. +* Added :doc:`architecture documentation ` to help + on-board new contributors. 4.3.0 (2024-02-01) ------------------ diff --git a/docs/contributing.rst b/docs/contributing.rst index 5e11ee603..55d9a5ca7 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -48,6 +48,12 @@ For convenience, there's an alias for the second command:: Look at ``example/settings.py`` for running the example with another database than SQLite. +Architecture +------------ + +There is high-level information on how the Django Debug Toolbar is structured +in the :doc:`architecture documentation `. + Tests ----- diff --git a/docs/index.rst b/docs/index.rst index e53703d4f..e72037045 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,3 +12,4 @@ Django Debug Toolbar commands changes contributing + architecture From cc48a140c283cf3aff1f79a5563206ca6ed2d8a8 Mon Sep 17 00:00:00 2001 From: Daniel Harding Date: Mon, 11 Mar 2024 15:53:14 +0300 Subject: [PATCH 015/200] Remove obsolete staticfiles check The toolbar had a check for misconfigured static files directories. However, Django 4.0 added its own check for this situation. Since the toolbar's minimum supported Django version is now 4.2, the toolbar's check is made redundant by Django's check and can thus be removed. --- debug_toolbar/panels/staticfiles.py | 25 ------------------- docs/changes.rst | 4 ++++ tests/panels/test_staticfiles.py | 37 ----------------------------- tests/test_checks.py | 23 ------------------ 4 files changed, 4 insertions(+), 85 deletions(-) diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py index 5f9efb5c3..2eed2efa0 100644 --- a/debug_toolbar/panels/staticfiles.py +++ b/debug_toolbar/panels/staticfiles.py @@ -4,7 +4,6 @@ from django.conf import settings from django.contrib.staticfiles import finders, storage -from django.core.checks import Warning from django.utils.functional import LazyObject from django.utils.translation import gettext_lazy as _, ngettext @@ -178,27 +177,3 @@ def get_staticfiles_apps(self): if app not in apps: apps.append(app) return apps - - @classmethod - def run_checks(cls): - """ - Check that the integration is configured correctly for the panel. - - Specifically look for static files that haven't been collected yet. - - Return a list of :class: `django.core.checks.CheckMessage` instances. - """ - errors = [] - for finder in finders.get_finders(): - try: - for path, finder_storage in finder.list([]): - finder_storage.path(path) - except OSError: - errors.append( - Warning( - "debug_toolbar requires the STATICFILES_DIRS directories to exist.", - hint="Running manage.py collectstatic may help uncover the issue.", - id="debug_toolbar.staticfiles.W001", - ) - ) - return errors diff --git a/docs/changes.rst b/docs/changes.rst index fd79230af..5e6fbb24d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -13,6 +13,10 @@ Pending * Stayed on top of pre-commit hook updates. * Added :doc:`architecture documentation ` to help on-board new contributors. +* Removed the static file path validation check in + :class:`StaticFilesPanel ` + since that check is made redundant by a similar check in Django 4.0 and + later. 4.3.0 (2024-02-01) ------------------ diff --git a/tests/panels/test_staticfiles.py b/tests/panels/test_staticfiles.py index 32ed7ea61..0736d86ed 100644 --- a/tests/panels/test_staticfiles.py +++ b/tests/panels/test_staticfiles.py @@ -1,15 +1,8 @@ -import os -import unittest - -import django from django.conf import settings from django.contrib.staticfiles import finders -from django.test.utils import override_settings from ..base import BaseTestCase -PATH_DOES_NOT_EXIST = os.path.join(settings.BASE_DIR, "tests", "invalid_static") - class StaticFilesPanelTestCase(BaseTestCase): panel_id = "StaticFilesPanel" @@ -52,33 +45,3 @@ def test_insert_content(self): "django.contrib.staticfiles.finders.AppDirectoriesFinder", content ) self.assertValidHTML(content) - - @unittest.skipIf(django.VERSION >= (4,), "Django>=4 handles missing dirs itself.") - @override_settings( - STATICFILES_DIRS=[PATH_DOES_NOT_EXIST] + settings.STATICFILES_DIRS, - STATIC_ROOT=PATH_DOES_NOT_EXIST, - ) - def test_finder_directory_does_not_exist(self): - """Misconfigure the static files settings and verify the toolbar runs. - - The test case is that the STATIC_ROOT is in STATICFILES_DIRS and that - the directory of STATIC_ROOT does not exist. - """ - response = self.panel.process_request(self.request) - self.panel.generate_stats(self.request, response) - content = self.panel.content - self.assertIn( - "django.contrib.staticfiles.finders.AppDirectoriesFinder", content - ) - self.assertNotIn( - "django.contrib.staticfiles.finders.FileSystemFinder (2 files)", content - ) - self.assertEqual(self.panel.num_used, 0) - self.assertNotEqual(self.panel.num_found, 0) - expected_apps = ["django.contrib.admin", "debug_toolbar"] - if settings.USE_GIS: - expected_apps = ["django.contrib.gis"] + expected_apps - self.assertEqual(self.panel.get_staticfiles_apps(), expected_apps) - self.assertEqual( - self.panel.get_staticfiles_dirs(), finders.FileSystemFinder().locations - ) diff --git a/tests/test_checks.py b/tests/test_checks.py index 8e4f8e62f..b88787f9d 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -1,14 +1,8 @@ -import os -import unittest from unittest.mock import patch -import django -from django.conf import settings from django.core.checks import Warning, run_checks from django.test import SimpleTestCase, override_settings -PATH_DOES_NOT_EXIST = os.path.join(settings.BASE_DIR, "tests", "invalid_static") - class ChecksTestCase(SimpleTestCase): @override_settings( @@ -92,23 +86,6 @@ def test_check_middleware_classes_error(self): messages, ) - @unittest.skipIf(django.VERSION >= (4,), "Django>=4 handles missing dirs itself.") - @override_settings( - STATICFILES_DIRS=[PATH_DOES_NOT_EXIST], - ) - def test_panel_check_errors(self): - messages = run_checks() - self.assertEqual( - messages, - [ - Warning( - "debug_toolbar requires the STATICFILES_DIRS directories to exist.", - hint="Running manage.py collectstatic may help uncover the issue.", - id="debug_toolbar.staticfiles.W001", - ) - ], - ) - @override_settings(DEBUG_TOOLBAR_PANELS=[]) def test_panels_is_empty(self): errors = run_checks() From cfd480101424a24e6b7e8cc193f40f149e1cc333 Mon Sep 17 00:00:00 2001 From: Daniel Harding Date: Mon, 11 Mar 2024 15:35:11 +0300 Subject: [PATCH 016/200] Make tox pass selenium environment variables When running tox, pass through the user's DISPLAY and DJANGO_SELENIUM_TESTS environment variables, so that DJANGO_SELENIUM_TESTS=true tox will actually run the Selenuim integration tests. Without this change, the test suite never sees the DJANGO_SELENIUM_TESTS variable and thus skips the integration tests. Without DISPLAY, the integration tests will error out (unless CI is present in the environment to instruct the test suite to run the Selenium webdriver in headless mode). --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 67436888f..4910a9f6b 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,8 @@ passenv= DB_PASSWORD DB_HOST DB_PORT + DISPLAY + DJANGO_SELENIUM_TESTS GITHUB_* setenv = PYTHONPATH = {toxinidir} From 68039c6833410a8f27ccd0d5b574c14c2cb792e8 Mon Sep 17 00:00:00 2001 From: Daniel Harding Date: Mon, 11 Mar 2024 19:47:26 +0300 Subject: [PATCH 017/200] Simplify default OBSERVE_REQUEST_CALLBACK behavior The previous implementation of observe_request(), the default callback for OBSERVE_REQUEST_CALLBACK, was checking for DebugToolbar.is_toolbar_request() and returning True only if that method returned False. However, this is actually unneeded, because DebugToolbarMiddleware never instruments its own requests. If DebugToolbar.is_toolbar_request() returns True for a given request, DebugToolbarMiddleware will return early without performing any instrumentation or processing on the request [1]. In this case, the OBSERVE_REQUEST_CALLBACK callback never gets called, because it only gets called via the HistoryPanel.get_headers() method [2]. The .get_headers() method is only called by DebugToolbarMiddleware after the early return mentioned above [3]. Thus if OBSERVE_REQUEST_CALLBACK is called, it must be the case that DebugToolbar.is_toolbar_request() is False for the current request. Therefore observe_request() can be simplified to always return True without changing its behavior. [1] https://github.com/jazzband/django-debug-toolbar/blob/c688ce4ad7d18c5ecb800869298ca8cf6c08be1d/debug_toolbar/middleware.py#L48-L49 [2] https://github.com/jazzband/django-debug-toolbar/blob/c688ce4ad7d18c5ecb800869298ca8cf6c08be1d/debug_toolbar/panels/history/panel.py#L23-L29 [3] https://github.com/jazzband/django-debug-toolbar/blob/c688ce4ad7d18c5ecb800869298ca8cf6c08be1d/debug_toolbar/middleware.py#L76-L77 --- debug_toolbar/toolbar.py | 2 +- docs/configuration.rst | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py index 11f8a1daa..fc07543b5 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -185,4 +185,4 @@ def observe_request(request): """ Determine whether to update the toolbar from a client side request. """ - return not DebugToolbar.is_toolbar_request(request) + return True diff --git a/docs/configuration.rst b/docs/configuration.rst index 8271092ca..2109e7c52 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -146,9 +146,8 @@ Toolbar options Default: ``'debug_toolbar.toolbar.observe_request'`` This is the dotted path to a function used for determining whether the - toolbar should update on AJAX requests or not. The default checks are that - the request doesn't originate from the toolbar itself, EG that - ``is_toolbar_request`` is false for a given request. + toolbar should update on AJAX requests or not. The default implementation + always returns ``True``. .. _TOOLBAR_LANGUAGE: From cfbad48862e2bbff2a614206ed77dbfd91ec315e Mon Sep 17 00:00:00 2001 From: Daniel Harding Date: Mon, 11 Mar 2024 16:43:23 +0300 Subject: [PATCH 018/200] Deprecate OBSERVE_REQUEST_CALLBACK With the addition of the UPDATE_ON_FETCH setting, the OBSERVE_REQUEST_CALLBACK setting is redundant. Thus add a check debug_toolbar.W008 to warn if it is present in DEBUG_TOOLBAR_CONFIG, but do not remove it yet. See #1886 --- debug_toolbar/apps.py | 15 +++++++++++++++ docs/changes.rst | 3 +++ docs/checks.rst | 3 +++ docs/configuration.rst | 5 +++++ tests/test_checks.py | 8 ++++++++ 5 files changed, 34 insertions(+) diff --git a/debug_toolbar/apps.py b/debug_toolbar/apps.py index 0a10a4b08..05cd35ae3 100644 --- a/debug_toolbar/apps.py +++ b/debug_toolbar/apps.py @@ -206,3 +206,18 @@ def js_mimetype_check(app_configs, **kwargs): ) ] return [] + + +@register() +def check_settings(app_configs, **kwargs): + errors = [] + USER_CONFIG = getattr(settings, "DEBUG_TOOLBAR_CONFIG", {}) + if "OBSERVE_REQUEST_CALLBACK" in USER_CONFIG: + errors.append( + Warning( + "The deprecated OBSERVE_REQUEST_CALLBACK setting is present in DEBUG_TOOLBAR_CONFIG.", + hint="Use the UPDATE_ON_FETCH and/or SHOW_TOOLBAR_CALLBACK settings instead.", + id="debug_toolbar.W008", + ) + ) + return errors diff --git a/docs/changes.rst b/docs/changes.rst index 5e6fbb24d..b461fbb40 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -17,6 +17,9 @@ Pending :class:`StaticFilesPanel ` since that check is made redundant by a similar check in Django 4.0 and later. +* Deprecated the ``OBSERVE_REQUEST_CALLBACK`` setting and added check + ``debug_toolbar.W008`` to warn when it is present in + ``DEBUG_TOOLBAR_SETTINGS``. 4.3.0 (2024-02-01) ------------------ diff --git a/docs/checks.rst b/docs/checks.rst index 6ed1e88f4..1c41d04fc 100644 --- a/docs/checks.rst +++ b/docs/checks.rst @@ -21,3 +21,6 @@ Django Debug Toolbar setup and configuration: * **debug_toolbar.W007**: JavaScript files are resolving to the wrong content type. Refer to :external:ref:`Django's explanation of mimetypes on Windows `. +* **debug_toolbar.W008**: The deprecated ``OBSERVE_REQUEST_CALLBACK`` setting + is present in ``DEBUG_TOOLBAR_CONFIG``. Use the ``UPDATE_ON_FETCH`` and/or + ``SHOW_TOOLBAR_CALLBACK`` settings instead. diff --git a/docs/configuration.rst b/docs/configuration.rst index 2109e7c52..b7db25900 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -145,6 +145,11 @@ Toolbar options Default: ``'debug_toolbar.toolbar.observe_request'`` + .. note:: + + This setting is deprecated in favor of the ``UPDATE_ON_FETCH`` and + ``SHOW_TOOLBAR_CALLBACK`` settings. + This is the dotted path to a function used for determining whether the toolbar should update on AJAX requests or not. The default implementation always returns ``True``. diff --git a/tests/test_checks.py b/tests/test_checks.py index b88787f9d..d04f8fc87 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -235,3 +235,11 @@ def test_check_w007_invalid(self, mocked_guess_type): ) ], ) + + @override_settings( + DEBUG_TOOLBAR_CONFIG={"OBSERVE_REQUEST_CALLBACK": lambda request: False} + ) + def test_observe_request_callback_specified(self): + errors = run_checks() + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0].id, "debug_toolbar.W008") From b3cb611cf6258ca13c8309e00e4543055d517326 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:24:23 +0100 Subject: [PATCH 019/200] [pre-commit.ci] pre-commit autoupdate (#1896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-eslint: v9.0.0-beta.1 → v9.0.0-beta.2](https://github.com/pre-commit/mirrors-eslint/compare/v9.0.0-beta.1...v9.0.0-beta.2) - [github.com/astral-sh/ruff-pre-commit: v0.3.0 → v0.3.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.0...v0.3.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25efec746..e1233feea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: args: - --trailing-comma=es5 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.0.0-beta.1 + rev: v9.0.0-beta.2 hooks: - id: eslint additional_dependencies: @@ -44,7 +44,7 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.0' + rev: 'v0.3.2' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 30976920b26068842c4cea35e869fa47947aeed7 Mon Sep 17 00:00:00 2001 From: Daniel Harding Date: Mon, 18 Mar 2024 10:05:27 +0300 Subject: [PATCH 020/200] Allow more control over tox Selenium tests (#1897) Instead of unconditionally enabling Selenium tests for the py311-dj42-postgresql tox environment, enable them by default for that environment but allow them to be disabled if the user's DJANGO_SELENIUM_TESTS environment variable is empty. This will allow the user to run DJANGO_SELENIUM_TESTS= tox to run the full tox test suite with Selenium tests disabled. --- docs/contributing.rst | 6 ++++++ tests/test_integration.py | 2 +- tox.ini | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 55d9a5ca7..0021a88fa 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -85,6 +85,12 @@ or by setting the ``DJANGO_SELENIUM_TESTS`` environment variable:: $ DJANGO_SELENIUM_TESTS=true make coverage $ DJANGO_SELENIUM_TESTS=true tox +Note that by default, ``tox`` enables the Selenium tests for a single test +environment. To run the entire ``tox`` test suite with all Selenium tests +disabled, run the following:: + + $ DJANGO_SELENIUM_TESTS= tox + To test via ``tox`` against other databases, you'll need to create the user, database and assign the proper permissions. For PostgreSQL in a ``psql`` shell (note this allows the debug_toolbar user the permission to create diff --git a/tests/test_integration.py b/tests/test_integration.py index fee67b7d1..127406df8 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -547,7 +547,7 @@ def test_auth_login_view_without_redirect(self): @unittest.skipIf(webdriver is None, "selenium isn't installed") @unittest.skipUnless( - "DJANGO_SELENIUM_TESTS" in os.environ, "selenium tests not requested" + os.environ.get("DJANGO_SELENIUM_TESTS"), "selenium tests not requested" ) @override_settings(DEBUG=True) class DebugToolbarLiveTestCase(StaticLiveServerTestCase): diff --git a/tox.ini b/tox.ini index 4910a9f6b..a0e72827a 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ passenv= setenv = PYTHONPATH = {toxinidir} PYTHONWARNINGS = d - py311-dj42-postgresql: DJANGO_SELENIUM_TESTS = true + py311-dj42-postgresql: DJANGO_SELENIUM_TESTS = {env:DJANGO_SELENIUM_TESTS:true} DB_NAME = {env:DB_NAME:debug_toolbar} DB_USER = {env:DB_USER:debug_toolbar} DB_HOST = {env:DB_HOST:localhost} From d3f4dbddd2f140df89b8423d2bad80c80a348dff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 19:11:34 +0100 Subject: [PATCH 021/200] [pre-commit.ci] pre-commit autoupdate (#1898) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1233feea..af496bb99 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.2' + rev: 'v0.3.3' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From e8848dc77f3aefdb49731d4450f598d713d7f75d Mon Sep 17 00:00:00 2001 From: Elineda Date: Thu, 21 Mar 2024 01:59:29 +0100 Subject: [PATCH 022/200] Docs > Add a note on the profiling panel doc (#1899) Add a note on the profiling panel document about python 3.12 and later. --- docs/changes.rst | 2 ++ docs/panels.rst | 4 ++++ docs/spelling_wordlist.txt | 2 ++ 3 files changed, 8 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index b461fbb40..5e2b4081f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -20,6 +20,8 @@ Pending * Deprecated the ``OBSERVE_REQUEST_CALLBACK`` setting and added check ``debug_toolbar.W008`` to warn when it is present in ``DEBUG_TOOLBAR_SETTINGS``. +* Add a note on the profiling panel about using Python 3.12 and later + about needing ``--nothreading`` 4.3.0 (2024-02-01) ------------------ diff --git a/docs/panels.rst b/docs/panels.rst index db4e9311f..33359ea46 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -123,6 +123,10 @@ Profiling information for the processing of the request. This panel is included but inactive by default. You can activate it by default with the ``DISABLE_PANELS`` configuration option. +For version of Python 3.12 and later you need to use +``python -m manage runserver --nothreading`` +Concurrent requests don't work with the profiling panel. + The panel will include all function calls made by your project if you're using the setting ``settings.BASE_DIR`` to point to your project's root directory. If a function is in a file within that directory and does not include diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 436977bdc..829ff9bec 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -36,6 +36,7 @@ mousedown mouseup multi neo +nothreading paddings pre profiler @@ -47,6 +48,7 @@ pyupgrade querysets refactoring resizing +runserver spellchecking spooler stacktrace From 4c4f767839d3ea4376beeb35479ea20f8ced506b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 30 Mar 2024 11:00:08 +0100 Subject: [PATCH 023/200] [pre-commit.ci] pre-commit autoupdate (#1900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-eslint: v9.0.0-beta.2 → v9.0.0-rc.0](https://github.com/pre-commit/mirrors-eslint/compare/v9.0.0-beta.2...v9.0.0-rc.0) - [github.com/astral-sh/ruff-pre-commit: v0.3.3 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.3...v0.3.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af496bb99..3ed467c58 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: args: - --trailing-comma=es5 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.0.0-beta.2 + rev: v9.0.0-rc.0 hooks: - id: eslint additional_dependencies: @@ -44,7 +44,7 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.3' + rev: 'v0.3.4' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 6d0535fd09ba06ef099837eb3ed14db332181a3c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 07:57:26 +0200 Subject: [PATCH 024/200] [pre-commit.ci] pre-commit autoupdate (#1902) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) - [github.com/pre-commit/mirrors-eslint: v9.0.0-rc.0 → v9.0.0](https://github.com/pre-commit/mirrors-eslint/compare/v9.0.0-rc.0...v9.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ed467c58..68f03cc27 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-toml - id: check-yaml @@ -32,7 +32,7 @@ repos: args: - --trailing-comma=es5 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.0.0-rc.0 + rev: v9.0.0 hooks: - id: eslint additional_dependencies: @@ -44,7 +44,7 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.4' + rev: 'v0.3.5' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From a36bbba8970976e1b71c56b97da19e2a88b3b3c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:20:10 +0200 Subject: [PATCH 025/200] [pre-commit.ci] pre-commit autoupdate (#1907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.3.7) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68f03cc27..1eb0a7df1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.5' + rev: 'v0.3.7' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 2f4c47177d8a11e62e61166da8dde30f6a796c59 Mon Sep 17 00:00:00 2001 From: Velda Kiara <32552296+VeldaKiara@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:22:18 +0300 Subject: [PATCH 026/200] 'djdt' is not a registered namespace #1405 (#1889) * updated the change to the changes.rst file * solution to djdt registered namespace,update docs, system checks and tests * removing test in the if statements * Add basic test to example app and example_test to make commands. * Update check for toolbar and tests. * update check using with, combine the four tests to one, update default config * update installation files and remove patch from namespace check * Clean-up the changelog * Add docs for the IS_RUNNING_TESTS setting Co-authored-by: Matthias Kestenholz * Change the code for the toolbar testing error to E001 * Reduce number of .settings calls and document config update. --------- Co-authored-by: Tim Schilling Co-authored-by: Matthias Kestenholz --- Makefile | 3 +++ debug_toolbar/apps.py | 28 +++++++++++++++++++++--- debug_toolbar/settings.py | 2 ++ docs/changes.rst | 10 +++++++++ docs/configuration.rst | 11 ++++++++++ docs/installation.rst | 6 +++++ example/settings.py | 20 ++++++++++++++--- example/test_views.py | 12 ++++++++++ example/urls.py | 7 +++++- tests/settings.py | 4 +++- tests/test_checks.py | 46 ++++++++++++++++++++++++++++++++++++--- 11 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 example/test_views.py diff --git a/Makefile b/Makefile index 1600496e5..24b59ab95 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ example: --noinput --username="$(USER)" --email="$(USER)@mailinator.com" python example/manage.py runserver +example_test: + python example/manage.py test example + test: DJANGO_SETTINGS_MODULE=tests.settings \ python -m django test $${TEST_ARGS:-tests} diff --git a/debug_toolbar/apps.py b/debug_toolbar/apps.py index 05cd35ae3..a2e977d84 100644 --- a/debug_toolbar/apps.py +++ b/debug_toolbar/apps.py @@ -3,7 +3,7 @@ from django.apps import AppConfig from django.conf import settings -from django.core.checks import Warning, register +from django.core.checks import Error, Warning, register from django.middleware.gzip import GZipMiddleware from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ @@ -177,7 +177,7 @@ def check_panels(app_configs, **kwargs): return errors -@register() +@register def js_mimetype_check(app_configs, **kwargs): """ Check that JavaScript files are resolving to the correct content type. @@ -208,7 +208,29 @@ def js_mimetype_check(app_configs, **kwargs): return [] -@register() +@register +def debug_toolbar_installed_when_running_tests_check(app_configs, **kwargs): + """ + Check that the toolbar is not being used when tests are running + """ + if not settings.DEBUG and dt_settings.get_config()["IS_RUNNING_TESTS"]: + return [ + Error( + "The Django Debug Toolbar can't be used with tests", + hint="Django changes the DEBUG setting to False when running " + "tests. By default the Django Debug Toolbar is installed because " + "DEBUG is set to True. For most cases, you need to avoid installing " + "the toolbar when running tests. If you feel this check is in error, " + "you can set `DEBUG_TOOLBAR_CONFIG['IS_RUNNING_TESTS'] = False` to " + "bypass this check.", + id="debug_toolbar.E001", + ) + ] + else: + return [] + + +@register def check_settings(app_configs, **kwargs): errors = [] USER_CONFIG = getattr(settings, "DEBUG_TOOLBAR_CONFIG", {}) diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index 1df24527d..868d50a90 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -1,3 +1,4 @@ +import sys import warnings from functools import lru_cache @@ -42,6 +43,7 @@ "SQL_WARNING_THRESHOLD": 500, # milliseconds "OBSERVE_REQUEST_CALLBACK": "debug_toolbar.toolbar.observe_request", "TOOLBAR_LANGUAGE": None, + "IS_RUNNING_TESTS": "test" in sys.argv, "UPDATE_ON_FETCH": False, } diff --git a/docs/changes.rst b/docs/changes.rst index 5e2b4081f..997a997f2 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -22,6 +22,16 @@ Pending ``DEBUG_TOOLBAR_SETTINGS``. * Add a note on the profiling panel about using Python 3.12 and later about needing ``--nothreading`` +* Added ``IS_RUNNING_TESTS`` setting to allow overriding the + ``debug_toolbar.E001`` check to avoid including the toolbar when running + tests. +* Fixed the bug causing ``'djdt' is not a registered namespace`` and updated + docs to help in initial configuration while running tests. +* Added a link in the installation docs to a more complete installation + example in the example app. +* Added check to prevent the toolbar from being installed when tests + are running. +* Added test to example app and command to run the example app's tests. 4.3.0 (2024-02-01) ------------------ diff --git a/docs/configuration.rst b/docs/configuration.rst index b7db25900..2af0b7fa4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -72,6 +72,17 @@ Toolbar options The toolbar searches for this string in the HTML and inserts itself just before. +* ``IS_RUNNING_TESTS`` + + Default: ``"test" in sys.argv`` + + This setting whether the application is running tests. If this resolves to + ``True``, the toolbar will prevent you from running tests. This should only + be changed if your test command doesn't include ``test`` or if you wish to + test your application with the toolbar configured. If you do wish to test + your application with the toolbar configured, set this setting to + ``False``. + .. _RENDER_PANELS: * ``RENDER_PANELS`` diff --git a/docs/installation.rst b/docs/installation.rst index 1f2e1f119..3644bdd5c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -81,6 +81,11 @@ Add ``"debug_toolbar"`` to your ``INSTALLED_APPS`` setting: "debug_toolbar", # ... ] +.. note:: Check out the configuration example in the + `example app + `_ + to learn how to set up the toolbar to function smoothly while running + your tests. 4. Add the URLs ^^^^^^^^^^^^^^^ @@ -99,6 +104,7 @@ Add django-debug-toolbar's URLs to your project's URLconf: This example uses the ``__debug__`` prefix, but you can use any prefix that doesn't clash with your application's URLs. + 5. Add the Middleware ^^^^^^^^^^^^^^^^^^^^^ diff --git a/example/settings.py b/example/settings.py index d2bd57387..1508b5a29 100644 --- a/example/settings.py +++ b/example/settings.py @@ -1,12 +1,14 @@ """Django settings for example project.""" import os +import sys BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production + SECRET_KEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" DEBUG = True @@ -22,11 +24,9 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "debug_toolbar", ] MIDDLEWARE = [ - "debug_toolbar.middleware.DebugToolbarMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", @@ -61,7 +61,6 @@ WSGI_APPLICATION = "example.wsgi.application" -DEBUG_TOOLBAR_CONFIG = {"ROOT_TAG_EXTRA_ATTRS": "data-turbo-permanent hx-preserve"} # Cache and database @@ -97,3 +96,18 @@ } STATICFILES_DIRS = [os.path.join(BASE_DIR, "example", "static")] + + +# Only enable the toolbar when we're in debug mode and we're +# not running tests. Django will change DEBUG to be False for +# tests, so we can't rely on DEBUG alone. +ENABLE_DEBUG_TOOLBAR = DEBUG and "test" not in sys.argv +if ENABLE_DEBUG_TOOLBAR: + INSTALLED_APPS += [ + "debug_toolbar", + ] + MIDDLEWARE += [ + "debug_toolbar.middleware.DebugToolbarMiddleware", + ] + # Customize the config to support turbo and htmx boosting. + DEBUG_TOOLBAR_CONFIG = {"ROOT_TAG_EXTRA_ATTRS": "data-turbo-permanent hx-preserve"} diff --git a/example/test_views.py b/example/test_views.py new file mode 100644 index 000000000..c3a8b96b0 --- /dev/null +++ b/example/test_views.py @@ -0,0 +1,12 @@ +# Add tests to example app to check how the toolbar is used +# when running tests for a project. +# See https://github.com/jazzband/django-debug-toolbar/issues/1405 + +from django.test import TestCase +from django.urls import reverse + + +class ViewTestCase(TestCase): + def test_index(self): + response = self.client.get(reverse("home")) + assert response.status_code == 200 diff --git a/example/urls.py b/example/urls.py index da52601f8..7569a57f9 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib import admin from django.urls import include, path from django.views.generic import TemplateView @@ -33,5 +34,9 @@ ), path("admin/", admin.site.urls), path("ajax/increment", increment, name="ajax_increment"), - path("__debug__/", include("debug_toolbar.urls")), ] + +if settings.ENABLE_DEBUG_TOOLBAR: + urlpatterns += [ + path("__debug__/", include("debug_toolbar.urls")), + ] diff --git a/tests/settings.py b/tests/settings.py index b3c281242..269900c18 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -126,5 +126,7 @@ DEBUG_TOOLBAR_CONFIG = { # Django's test client sets wsgi.multiprocess to True inappropriately - "RENDER_PANELS": False + "RENDER_PANELS": False, + # IS_RUNNING_TESTS must be False even though we're running tests because we're running the toolbar's own tests. + "IS_RUNNING_TESTS": False, } diff --git a/tests/test_checks.py b/tests/test_checks.py index d04f8fc87..827886db1 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -1,8 +1,11 @@ from unittest.mock import patch -from django.core.checks import Warning, run_checks +from django.core.checks import Error, Warning, run_checks from django.test import SimpleTestCase, override_settings +from debug_toolbar import settings as dt_settings +from debug_toolbar.apps import debug_toolbar_installed_when_running_tests_check + class ChecksTestCase(SimpleTestCase): @override_settings( @@ -97,7 +100,7 @@ def test_panels_is_empty(self): hint="Set DEBUG_TOOLBAR_PANELS to a non-empty list in your " "settings.py.", id="debug_toolbar.W005", - ) + ), ], ) @@ -236,8 +239,45 @@ def test_check_w007_invalid(self, mocked_guess_type): ], ) + def test_debug_toolbar_installed_when_running_tests(self): + with self.settings(DEBUG=True): + # Update the config options because self.settings() + # would require redefining DEBUG_TOOLBAR_CONFIG entirely. + dt_settings.get_config()["IS_RUNNING_TESTS"] = True + errors = debug_toolbar_installed_when_running_tests_check(None) + self.assertEqual(len(errors), 0) + + dt_settings.get_config()["IS_RUNNING_TESTS"] = False + errors = debug_toolbar_installed_when_running_tests_check(None) + self.assertEqual(len(errors), 0) + with self.settings(DEBUG=False): + dt_settings.get_config()["IS_RUNNING_TESTS"] = False + errors = debug_toolbar_installed_when_running_tests_check(None) + self.assertEqual(len(errors), 0) + + dt_settings.get_config()["IS_RUNNING_TESTS"] = True + errors = debug_toolbar_installed_when_running_tests_check(None) + self.assertEqual( + errors, + [ + Error( + "The Django Debug Toolbar can't be used with tests", + hint="Django changes the DEBUG setting to False when running " + "tests. By default the Django Debug Toolbar is installed because " + "DEBUG is set to True. For most cases, you need to avoid installing " + "the toolbar when running tests. If you feel this check is in error, " + "you can set `DEBUG_TOOLBAR_CONFIG['IS_RUNNING_TESTS'] = False` to " + "bypass this check.", + id="debug_toolbar.E001", + ) + ], + ) + @override_settings( - DEBUG_TOOLBAR_CONFIG={"OBSERVE_REQUEST_CALLBACK": lambda request: False} + DEBUG_TOOLBAR_CONFIG={ + "OBSERVE_REQUEST_CALLBACK": lambda request: False, + "IS_RUNNING_TESTS": False, + } ) def test_observe_request_callback_specified(self): errors = run_checks() From 213460066413984344e2de117ce7468cd7e257eb Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Tue, 30 Apr 2024 08:44:12 -0600 Subject: [PATCH 027/200] Remove unnecessary GitHub Graph info (#1910) * Remove unnecessary GitHub Graph info This code is no longer needed now that the GitHub dependency graph shipped support for `pyproject.toml`: https://github.com/orgs/community/discussions/6456#discussioncomment-5244057 --- setup.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/setup.py b/setup.py index de31ca34f..3893c8d49 100755 --- a/setup.py +++ b/setup.py @@ -2,8 +2,6 @@ import sys -from setuptools import setup - sys.stderr.write( """\ =============================== @@ -14,10 +12,3 @@ """ ) sys.exit(1) - -# The code below will never execute, however is required to -# display the "Used by" section on the GitHub repository. -# -# See: https://github.com/github/feedback/discussions/6456 - -setup(name="django-debug-toolbar") From c1463a5bbed7de4865c5a2967d02c81dccbf9036 Mon Sep 17 00:00:00 2001 From: Aman Pandey Date: Tue, 7 May 2024 18:55:19 +0530 Subject: [PATCH 028/200] New coverage.yml for code coverage (#1912) * new coverage.yml * match the job name * remove upload code coverage config --- .github/workflows/coverage.yml | 33 +++++++++++++++++++++++ .github/workflows/test.yml | 49 ---------------------------------- 2 files changed, 33 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000..a0722f0ac --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,33 @@ +# .github/workflows/coverage.yml +name: Post coverage comment + +on: + workflow_run: + workflows: ["Test"] + types: + - completed + +jobs: + test: + name: Run tests & display coverage + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + permissions: + # Gives the action the necessary permissions for publishing new + # comments in pull requests. + pull-requests: write + # Gives the action the necessary permissions for editing existing + # comments (to avoid publishing multiple comments in the same PR) + contents: write + # Gives the action the necessary permissions for looking up the + # workflow that launched this workflow, and download the related + # artifact that contains the comment to be published + actions: read + steps: + # DO NOT run actions/checkout here, for security reasons + # For details, refer to https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + - name: Post comment + uses: py-cov-action/python-coverage-comment-action@v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72b40d010..cd5d8dd8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,11 +66,6 @@ jobs: DB_HOST: 127.0.0.1 DB_PORT: 3306 - - name: Upload coverage data - uses: actions/upload-artifact@v4 - with: - name: coverage-data-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.arch }}-mysql - path: ".coverage.*" postgres: runs-on: ubuntu-latest @@ -144,12 +139,6 @@ jobs: DB_HOST: localhost DB_PORT: 5432 - - name: Upload coverage data - uses: actions/upload-artifact@v4 - with: - name: coverage-data-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.database }} - path: ".coverage.*" - sqlite: runs-on: ubuntu-latest strategy: @@ -192,44 +181,6 @@ jobs: DB_BACKEND: sqlite3 DB_NAME: ":memory:" - - name: Upload coverage data - uses: actions/upload-artifact@v4 - with: - name: coverage-data-${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.arch }}-sqlite - path: ".coverage.*" - - coverage: - name: Check coverage. - runs-on: "ubuntu-latest" - needs: [sqlite, mysql, postgres] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - # Use latest, so it understands all syntax. - python-version: "3.11" - - - run: python -m pip install --upgrade coverage[toml] - - - name: Download coverage data. - uses: actions/download-artifact@v4 - with: - pattern: coverage-data-* - merge-multiple: true - - - name: Combine coverage & check percentage - run: | - python -m coverage combine - python -m coverage html - python -m coverage report - - - name: Upload HTML report if check failed. - uses: actions/upload-artifact@v4 - with: - name: html-report - path: htmlcov - if: ${{ failure() }} - lint: runs-on: ubuntu-latest strategy: From 7271cac9eacb7870200d6bc965c84eb91ba36714 Mon Sep 17 00:00:00 2001 From: Eduardo Leyva <75857767+TheRealVizard@users.noreply.github.com> Date: Tue, 14 May 2024 09:20:31 -0400 Subject: [PATCH 029/200] Dark mode support (#1913) --- debug_toolbar/settings.py | 1 + .../static/debug_toolbar/css/toolbar.css | 160 +++++++++++++----- .../static/debug_toolbar/js/toolbar.js | 27 ++- .../templates/debug_toolbar/base.html | 8 +- .../includes/theme_selector.html | 47 +++++ docs/changes.rst | 3 + docs/configuration.rst | 16 +- tests/test_integration.py | 26 +++ 8 files changed, 242 insertions(+), 46 deletions(-) create mode 100644 debug_toolbar/templates/debug_toolbar/includes/theme_selector.html diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index 868d50a90..ca7036c34 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -45,6 +45,7 @@ "TOOLBAR_LANGUAGE": None, "IS_RUNNING_TESTS": "test" in sys.argv, "UPDATE_ON_FETCH": False, + "DEFAULT_THEME": "auto", } diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index a35286a1f..170cc3d5f 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -1,5 +1,6 @@ /* Variable definitions */ -:root { +:root, +#djDebug[data-theme="light"] { /* Font families are the same as in Django admin/css/base.css */ --djdt-font-family-primary: "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", @@ -9,12 +10,79 @@ "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + + color-scheme: light; + --djdt-font-color: black; + --djdt-background-color: white; + --djdt-panel-content-background-color: #eee; + --djdt-panel-content-table-background-color: var(--djdt-background-color); + --djdt-panel-title-background-color: #ffc; + --djdt-djdt-panel-content-table-strip-background-color: #f5f5f5; + --djdt--highlighted-background-color: lightgrey; + --djdt-toggle-template-background-color: #bbb; + + --djdt-sql-font-color: #333; + --djdt-pre-text-color: #555; + --djdt-path-and-locals: #777; + --djdt-stack-span-color: black; + --djdt-template-highlight-color: #333; + + --djdt-table-border-color: #ccc; + --djdt-button-border-color: var(--djdt-table-border-color); + --djdt-pre-border-color: var(--djdt-table-border-color); + --djdt-raw-border-color: var(--djdt-table-border-color); +} + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --djdt-font-color: #8393a7; + --djdt-background-color: #1e293bff; + --djdt-panel-content-background-color: #0f1729ff; + --djdt-panel-title-background-color: #242432; + --djdt-djdt-panel-content-table-strip-background-color: #324154ff; + --djdt--highlighted-background-color: #2c2a7dff; + --djdt-toggle-template-background-color: #282755; + + --djdt-sql-font-color: var(--djdt-font-color); + --djdt-pre-text-color: var(--djdt-font-color); + --djdt-path-and-locals: #65758cff; + --djdt-stack-span-color: #7c8fa4; + --djdt-template-highlight-color: var(--djdt-stack-span-color); + + --djdt-table-border-color: #324154ff; + --djdt-button-border-color: var(--djdt-table-border-color); + --djdt-pre-border-color: var(--djdt-table-border-color); + --djdt-raw-border-color: var(--djdt-table-border-color); + } +} + +#djDebug[data-theme="dark"] { + color-scheme: dark; + --djdt-font-color: #8393a7; + --djdt-background-color: #1e293bff; + --djdt-panel-content-background-color: #0f1729ff; + --djdt-panel-title-background-color: #242432; + --djdt-djdt-panel-content-table-strip-background-color: #324154ff; + --djdt--highlighted-background-color: #2c2a7dff; + --djdt-toggle-template-background-color: #282755; + + --djdt-sql-font-color: var(--djdt-font-color); + --djdt-pre-text-color: var(--djdt-font-color); + --djdt-path-and-locals: #65758cff; + --djdt-stack-span-color: #7c8fa4; + --djdt-template-highlight-color: var(--djdt-stack-span-color); + + --djdt-table-border-color: #324154ff; + --djdt-button-border-color: var(--djdt-table-border-color); + --djdt-pre-border-color: var(--djdt-table-border-color); + --djdt-raw-border-color: var(--djdt-table-border-color); } /* Debug Toolbar CSS Reset, adapted from Eric Meyer's CSS Reset */ #djDebug { - color: #000; - background: #fff; + color: var(--djdt-font-color); + background: var(--djdt-background-color); } #djDebug, #djDebug div, @@ -87,7 +155,7 @@ outline: 0; font-size: 12px; line-height: 1.5em; - color: #000; + color: var(--djdt-font-color); vertical-align: baseline; background-color: transparent; font-family: var(--djdt-font-family-primary); @@ -100,7 +168,7 @@ #djDebug button { background-color: #eee; background-image: linear-gradient(to bottom, #eee, #cccccc); - border: 1px solid #ccc; + border: 1px solid var(--djdt-button-border-color); border-bottom: 1px solid #bbb; border-radius: 3px; color: #333; @@ -268,10 +336,10 @@ #djDebug pre { white-space: pre-wrap; - color: #555; - border: 1px solid #ccc; + color: var(--djdt-pre-text-color); + border: 1px solid var(--djdt-pre-border-color); border-collapse: collapse; - background-color: #fff; + background-color: var(--djdt-background-color); padding: 2px 3px; margin-bottom: 3px; } @@ -283,7 +351,7 @@ right: 220px; bottom: 0; left: 0px; - background-color: #eee; + background-color: var(--djdt-panel-content-background-color); color: #666; z-index: 100000000; } @@ -294,7 +362,7 @@ #djDebug .djDebugPanelTitle { position: absolute; - background-color: #ffc; + background-color: var(--djdt-panel-title-background-color); color: #666; padding-left: 20px; top: 0; @@ -357,16 +425,16 @@ } #djDebug .djdt-panelContent table { - border: 1px solid #ccc; + border: 1px solid var(--djdt-table-border-color); border-collapse: collapse; width: 100%; - background-color: #fff; + background-color: var(--djdt-panel-content-table-background-color); display: table; margin-top: 0.8em; overflow: auto; } #djDebug .djdt-panelContent tbody > tr:nth-child(odd):not(.djdt-highlighted) { - background-color: #f5f5f5; + background-color: var(--djdt-panel-content-table-strip-background-color); } #djDebug .djdt-panelContent tbody td, #djDebug .djdt-panelContent tbody th { @@ -392,7 +460,7 @@ } #djDebug .djTemplateContext { - background-color: #fff; + background-color: var(--djdt-background-color); } #djDebug .djdt-panelContent .djDebugClose { @@ -433,7 +501,7 @@ #djDebug a.toggleTemplate { padding: 4px; - background-color: #bbb; + background-color: var(--djdt-toggle-template-background-color); border-radius: 3px; } @@ -445,11 +513,11 @@ } #djDebug .djDebugCollapsed { - color: #333; + color: var(--djdt-sql-font-color); } #djDebug .djDebugUncollapsed { - color: #333; + color: var(--djdt-sql-font-color); } #djDebug .djUnselected { @@ -483,66 +551,66 @@ } #djDebug .highlight { - color: #000; + color: var(--djdt-font-color); } #djDebug .highlight .err { - color: #000; + color: var(--djdt-font-color); } /* Error */ #djDebug .highlight .g { - color: #000; + color: var(--djdt-font-color); } /* Generic */ #djDebug .highlight .k { - color: #000; + color: var(--djdt-font-color); font-weight: bold; } /* Keyword */ #djDebug .highlight .o { - color: #000; + color: var(--djdt-font-color); } /* Operator */ #djDebug .highlight .n { - color: #000; + color: var(--djdt-font-color); } /* Name */ #djDebug .highlight .mi { - color: #000; + color: var(--djdt-font-color); font-weight: bold; } /* Literal.Number.Integer */ #djDebug .highlight .l { - color: #000; + color: var(--djdt-font-color); } /* Literal */ #djDebug .highlight .x { - color: #000; + color: var(--djdt-font-color); } /* Other */ #djDebug .highlight .p { - color: #000; + color: var(--djdt-font-color); } /* Punctuation */ #djDebug .highlight .m { - color: #000; + color: var(--djdt-font-color); font-weight: bold; } /* Literal.Number */ #djDebug .highlight .s { - color: #333; + color: var(--djdt-template-highlight-color); } /* Literal.String */ #djDebug .highlight .w { color: #888888; } /* Text.Whitespace */ #djDebug .highlight .il { - color: #000; + color: var(--djdt-font-color); font-weight: bold; } /* Literal.Number.Integer.Long */ #djDebug .highlight .na { - color: #333; + color: var(--djdt-template-highlight-color); } /* Name.Attribute */ #djDebug .highlight .nt { - color: #000; + color: var(--djdt-font-color); font-weight: bold; } /* Name.Tag */ #djDebug .highlight .nv { - color: #333; + color: var(--djdt-template-highlight-color); } /* Name.Variable */ #djDebug .highlight .s2 { - color: #333; + color: var(--djdt-template-highlight-color); } /* Literal.String.Double */ #djDebug .highlight .cp { - color: #333; + color: var(--djdt-template-highlight-color); } /* Comment.Preproc */ #djDebug svg.djDebugLineChart { @@ -595,13 +663,13 @@ } #djDebug .djdt-stack span { - color: #000; + color: var(--djdt-stack-span-color); font-weight: bold; } #djDebug .djdt-stack span.djdt-path, #djDebug .djdt-stack pre.djdt-locals, #djDebug .djdt-stack pre.djdt-locals span { - color: #777; + color: var(--djdt-path-and-locals); font-weight: normal; } #djDebug .djdt-stack span.djdt-code { @@ -612,7 +680,7 @@ } #djDebug .djdt-raw { background-color: #fff; - border: 1px solid #ccc; + border: 1px solid var(--djdt-raw-border-color); margin-top: 0.8em; padding: 5px; white-space: pre-wrap; @@ -631,7 +699,7 @@ max-height: 100%; } #djDebug .djdt-highlighted { - background-color: lightgrey; + background-color: var(--djdt--highlighted-background-color); } #djDebug tr.djdt-highlighted.djdt-profile-row { background-color: #ffc; @@ -654,3 +722,17 @@ .djdt-hidden { display: none; } + +#djDebug #djDebugToolbar a#djToggleThemeButton { + display: flex; + align-items: center; + cursor: pointer; +} +#djToggleThemeButton > svg { + margin-left: auto; +} +#djDebug[data-theme="light"] #djToggleThemeButton svg.theme-light, +#djDebug[data-theme="dark"] #djToggleThemeButton svg.theme-dark, +#djDebug[data-theme="auto"] #djToggleThemeButton svg.theme-auto { + display: block; +} diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 199616336..067b5a312 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -1,4 +1,4 @@ -import { $$, ajax, replaceToolbarState, debounce } from "./utils.js"; +import { $$, ajax, debounce, replaceToolbarState } from "./utils.js"; function onKeyDown(event) { if (event.keyCode === 27) { @@ -213,6 +213,29 @@ const djdt = { if (djDebug.dataset.sidebarUrl !== undefined) { djdt.updateOnAjax(); } + + // Updates the theme using user settings + const userTheme = localStorage.getItem("djdt.user-theme"); + if (userTheme !== null) { + djDebug.setAttribute("data-theme", userTheme); + } + // Adds the listener to the Theme Toggle Button + $$.on(djDebug, "click", "#djToggleThemeButton", function () { + switch (djDebug.getAttribute("data-theme")) { + case "auto": + djDebug.setAttribute("data-theme", "light"); + localStorage.setItem("djdt.user-theme", "light"); + break; + case "light": + djDebug.setAttribute("data-theme", "dark"); + localStorage.setItem("djdt.user-theme", "dark"); + break; + default: /* dark is the default */ + djDebug.setAttribute("data-theme", "auto"); + localStorage.setItem("djdt.user-theme", "auto"); + break; + } + }); }, hidePanels() { const djDebug = getDebugElement(); @@ -276,7 +299,7 @@ const djdt = { storeId = encodeURIComponent(storeId); const dest = `${sidebarUrl}?store_id=${storeId}`; slowjax(dest).then(function (data) { - if (djdt.needUpdateOnFetch){ + if (djdt.needUpdateOnFetch) { replaceToolbarState(storeId, data); } }); diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index 6f4967f21..4867a834e 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -16,10 +16,16 @@ 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 }} data-update-on-fetch="{{ toolbar.config.UPDATE_ON_FETCH }}"> + {{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }} data-update-on-fetch="{{ toolbar.config.UPDATE_ON_FETCH }}" + data-theme="{{ toolbar.config.DEFAULT_THEME }}">

    Django Admin

    {% endcache %} diff --git a/example/urls.py b/example/urls.py index b64aa1c37..6dded2da7 100644 --- a/example/urls.py +++ b/example/urls.py @@ -7,6 +7,11 @@ urlpatterns = [ path("", TemplateView.as_view(template_name="index.html"), name="home"), + path( + "bad-form/", + TemplateView.as_view(template_name="bad_form.html"), + name="bad_form", + ), path("jquery/", TemplateView.as_view(template_name="jquery/index.html")), path("mootools/", TemplateView.as_view(template_name="mootools/index.html")), path("prototype/", TemplateView.as_view(template_name="prototype/index.html")), diff --git a/tests/panels/test_alerts.py b/tests/panels/test_alerts.py new file mode 100644 index 000000000..e61c8da12 --- /dev/null +++ b/tests/panels/test_alerts.py @@ -0,0 +1,101 @@ +from django.http import HttpResponse +from django.template import Context, Template + +from ..base import BaseTestCase + + +class AlertsPanelTestCase(BaseTestCase): + panel_id = "AlertsPanel" + + def test_alert_warning_display(self): + """ + Test that the panel (does not) display[s] an alert when there are + (no) problems. + """ + self.panel.record_stats({"alerts": []}) + self.assertNotIn("alerts", self.panel.nav_subtitle) + + self.panel.record_stats({"alerts": ["Alert 1", "Alert 2"]}) + self.assertIn("2 alerts", self.panel.nav_subtitle) + + def test_file_form_without_enctype_multipart_form_data(self): + """ + Test that the panel displays a form invalid message when there is + a file input but encoding not set to multipart/form-data. + """ + test_form = '
    ' + result = self.panel.check_invalid_file_form_configuration(test_form) + expected_error = ( + 'Form with id "test-form" contains file input, ' + 'but does not have the attribute enctype="multipart/form-data".' + ) + self.assertEqual(result[0]["alert"], expected_error) + self.assertEqual(len(result), 1) + + def test_file_form_no_id_without_enctype_multipart_form_data(self): + """ + Test that the panel displays a form invalid message when there is + a file input but encoding not set to multipart/form-data. + + This should use the message when the form has no id. + """ + test_form = '
    ' + result = self.panel.check_invalid_file_form_configuration(test_form) + expected_error = ( + "Form contains file input, but does not have " + 'the attribute enctype="multipart/form-data".' + ) + self.assertEqual(result[0]["alert"], expected_error) + self.assertEqual(len(result), 1) + + def test_file_form_with_enctype_multipart_form_data(self): + test_form = """
    + + """ + result = self.panel.check_invalid_file_form_configuration(test_form) + + self.assertEqual(len(result), 0) + + def test_file_form_with_enctype_multipart_form_data_in_button(self): + test_form = """
    + + + """ + result = self.panel.check_invalid_file_form_configuration(test_form) + + self.assertEqual(len(result), 0) + + def test_referenced_file_input_without_enctype_multipart_form_data(self): + test_file_input = """
    + """ + result = self.panel.check_invalid_file_form_configuration(test_file_input) + + expected_error = ( + 'Input element references form with id "test-form", ' + 'but the form does not have the attribute enctype="multipart/form-data".' + ) + self.assertEqual(result[0]["alert"], expected_error) + self.assertEqual(len(result), 1) + + def test_referenced_file_input_with_enctype_multipart_form_data(self): + test_file_input = """
    + + """ + result = self.panel.check_invalid_file_form_configuration(test_file_input) + + self.assertEqual(len(result), 0) + + def test_integration_file_form_without_enctype_multipart_form_data(self): + t = Template('
    ') + c = Context({}) + rendered_template = t.render(c) + response = HttpResponse(content=rendered_template) + + self.panel.generate_stats(self.request, response) + + self.assertIn("1 alert", self.panel.nav_subtitle) + self.assertIn( + "Form with id "test-form" contains file input, " + "but does not have the attribute enctype="multipart/form-data".", + self.panel.content, + ) diff --git a/tests/panels/test_history.py b/tests/panels/test_history.py index 2e0aa2179..4c5244934 100644 --- a/tests/panels/test_history.py +++ b/tests/panels/test_history.py @@ -75,6 +75,7 @@ class HistoryViewsTestCase(IntegrationTestCase): "SQLPanel", "StaticFilesPanel", "TemplatesPanel", + "AlertsPanel", "CachePanel", "SignalsPanel", "ProfilingPanel", From eff9128f2233f8c6341e1fba8a44d5db54cd9ae2 Mon Sep 17 00:00:00 2001 From: "Michael J. Nicholson" Date: Thu, 4 Jul 2024 18:29:51 +0200 Subject: [PATCH 051/200] Remove rem units from svg (#1942) * Use css properties for height and width --- .gitignore | 2 ++ debug_toolbar/static/debug_toolbar/css/toolbar.css | 2 ++ .../templates/debug_toolbar/includes/theme_selector.html | 6 ------ docs/changes.rst | 1 + 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index ee3559cc4..988922d50 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ htmlcov .tox geckodriver.log coverage.xml +.direnv/ +.envrc diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index c7e201d07..e495eeb0c 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -734,4 +734,6 @@ #djDebug[data-theme="dark"] #djToggleThemeButton svg.theme-dark, #djDebug[data-theme="auto"] #djToggleThemeButton svg.theme-auto { display: block; + height: 1rem; + width: 1rem; } diff --git a/debug_toolbar/templates/debug_toolbar/includes/theme_selector.html b/debug_toolbar/templates/debug_toolbar/includes/theme_selector.html index 372727900..926ff250b 100644 --- a/debug_toolbar/templates/debug_toolbar/includes/theme_selector.html +++ b/debug_toolbar/templates/debug_toolbar/includes/theme_selector.html @@ -2,8 +2,6 @@ aria-hidden="true" class="djdt-hidden theme-auto" fill="currentColor" - width="1rem" - height="1rem" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" @@ -15,8 +13,6 @@

    Index of Tests

    {% cache 10 index_cache %}
    " + - '' + - ""; + row.innerHTML = ` + + + +`; row.querySelector("rect").setAttribute( "width", getCSSWidth(stat, endStat) ); } else { // Render a point in time - row.innerHTML = - "" + - '' + - ""; + row.innerHTML = ` + + + +`; row.querySelector("rect").setAttribute("width", 2); } row.querySelector("rect").setAttribute("x", getLeft(stat)); diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index ee47025a1..79cfbdd58 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -116,7 +116,7 @@ const djdt = { const toggleClose = "-"; const openMe = this.textContent === toggleOpen; const name = this.dataset.toggleName; - const container = document.getElementById(name + "_" + id); + const container = document.getElementById(`${name}_${id}`); container .querySelectorAll(".djDebugCollapsed") .forEach(function (e) { @@ -129,7 +129,7 @@ const djdt = { }); const self = this; this.closest(".djDebugPanelContent") - .querySelectorAll(".djToggleDetails_" + id) + .querySelectorAll(`.djToggleDetails_${id}`) .forEach(function (e) { if (openMe) { e.classList.add("djSelected"); @@ -173,7 +173,7 @@ const djdt = { top = window.innerHeight - handle.offsetHeight; } - handle.style.top = top + "px"; + handle.style.top = `${top}px`; djdt.handleDragged = true; } } @@ -255,7 +255,7 @@ const djdt = { localStorage.getItem("djdt.top") || 265, window.innerHeight - handle.offsetWidth ); - handle.style.top = handleTop + "px"; + handle.style.top = `${handleTop}px`; }, hideToolbar() { djdt.hidePanels(); @@ -360,15 +360,15 @@ const djdt = { } document.cookie = [ - encodeURIComponent(key) + "=" + String(value), + `${encodeURIComponent(key)}=${String(value)}`, options.expires - ? "; expires=" + options.expires.toUTCString() + ? `; expires=${options.expires.toUTCString()}` : "", - options.path ? "; path=" + options.path : "", - options.domain ? "; domain=" + options.domain : "", + options.path ? `; path=${options.path}` : "", + options.domain ? `; domain=${options.domain}` : "", options.secure ? "; secure" : "", "samesite" in options - ? "; samesite=" + options.samesite + ? `; samesite=${options.samesite}` : "; samesite=lax", ].join(""); diff --git a/debug_toolbar/static/debug_toolbar/js/utils.js b/debug_toolbar/static/debug_toolbar/js/utils.js index b5f00e2ac..e2f03bb2b 100644 --- a/debug_toolbar/static/debug_toolbar/js/utils.js +++ b/debug_toolbar/static/debug_toolbar/js/utils.js @@ -77,21 +77,18 @@ function ajax(url, init) { return response.json().catch(function (error) { return Promise.reject( new Error( - "The response is a invalid Json object : " + error + `The response is a invalid Json object : ${error}` ) ); }); } return Promise.reject( - new Error(response.status + ": " + response.statusText) + new Error(`${response.status}: ${response.statusText}`) ); }) .catch(function (error) { const win = document.getElementById("djDebugWindow"); - win.innerHTML = - '

    ' + - error.message + - "

    "; + win.innerHTML = `

    ${error.message}

    `; $$.show(win); throw error; }); @@ -118,7 +115,7 @@ function replaceToolbarState(newStoreId, data) { const panel = document.getElementById(panelId); if (panel) { panel.outerHTML = data[panelId].content; - document.getElementById("djdt-" + panelId).outerHTML = + document.getElementById(`djdt-${panelId}`).outerHTML = data[panelId].button; } }); From b5eab559e4f3f077813f890ed9d7a8ae2684e786 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Sat, 1 Mar 2025 15:54:08 +0100 Subject: [PATCH 163/200] Prefer arrow functions The way how they do not override the value of 'this' makes implementing handlers and callbacks so nuch nicer. --- biome.json | 1 - .../static/debug_toolbar/js/history.js | 24 ++++----- .../static/debug_toolbar/js/toolbar.js | 53 +++++++++---------- .../static/debug_toolbar/js/utils.js | 48 ++++++++--------- 4 files changed, 59 insertions(+), 67 deletions(-) diff --git a/biome.json b/biome.json index bed293eb8..7d0bdd912 100644 --- a/biome.json +++ b/biome.json @@ -12,7 +12,6 @@ "rules": { "recommended": true, "complexity": { - "useArrowFunction": "off", "noForEach": "off" }, "suspicious": { diff --git a/debug_toolbar/static/debug_toolbar/js/history.js b/debug_toolbar/static/debug_toolbar/js/history.js index dc363d3ab..a0d339f2b 100644 --- a/debug_toolbar/static/debug_toolbar/js/history.js +++ b/debug_toolbar/static/debug_toolbar/js/history.js @@ -15,7 +15,7 @@ function difference(setA, setB) { */ function pluckData(nodes, key) { const data = []; - nodes.forEach(function (obj) { + nodes.forEach((obj) => { data.push(obj.dataset[key]); }); return data; @@ -29,18 +29,16 @@ function refreshHistory() { ); ajaxForm(formTarget) - .then(function (data) { + .then((data) => { // Remove existing rows first then re-populate with new data - container - .querySelectorAll("tr[data-store-id]") - .forEach(function (node) { - node.remove(); - }); - data.requests.forEach(function (request) { + container.querySelectorAll("tr[data-store-id]").forEach((node) => { + node.remove(); + }); + data.requests.forEach((request) => { container.innerHTML = request.content + container.innerHTML; }); }) - .then(function () { + .then(() => { const allIds = new Set( pluckData( container.querySelectorAll("tr[data-store-id]"), @@ -55,8 +53,8 @@ function refreshHistory() { lastRequestId, }; }) - .then(function (refreshInfo) { - refreshInfo.newIds.forEach(function (newId) { + .then((refreshInfo) => { + refreshInfo.newIds.forEach((newId) => { const row = container.querySelector( `tr[data-store-id="${newId}"]` ); @@ -84,7 +82,7 @@ function switchHistory(newStoreId) { } formTarget.closest("tr").classList.add("djdt-highlighted"); - ajaxForm(formTarget).then(function (data) { + ajaxForm(formTarget).then((data) => { if (Object.keys(data).length === 0) { const container = document.getElementById("djdtHistoryRequests"); container.querySelector( @@ -100,7 +98,7 @@ $$.on(djDebug, "click", ".switchHistory", function (event) { switchHistory(this.dataset.storeId); }); -$$.on(djDebug, "click", ".refreshHistory", function (event) { +$$.on(djDebug, "click", ".refreshHistory", (event) => { event.preventDefault(); refreshHistory(); }); diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 79cfbdd58..3e99e6ecb 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -47,7 +47,7 @@ const djdt = { ); url.searchParams.append("store_id", storeId); url.searchParams.append("panel_id", panelId); - ajax(url).then(function (data) { + ajax(url).then((data) => { inner.previousElementSibling.remove(); // Remove AJAX loader inner.innerHTML = data.content; $$.executeScripts(data.scripts); @@ -67,7 +67,7 @@ const djdt = { } } }); - $$.on(djDebug, "click", ".djDebugClose", function () { + $$.on(djDebug, "click", ".djDebugClose", () => { djdt.hideOneLevel(); }); $$.on( @@ -102,7 +102,7 @@ const djdt = { url = this.href; } - ajax(url, ajaxData).then(function (data) { + ajax(url, ajaxData).then((data) => { const win = document.getElementById("djDebugWindow"); win.innerHTML = data.content; $$.show(win); @@ -117,42 +117,37 @@ const djdt = { const openMe = this.textContent === toggleOpen; const name = this.dataset.toggleName; const container = document.getElementById(`${name}_${id}`); - container - .querySelectorAll(".djDebugCollapsed") - .forEach(function (e) { - $$.toggle(e, openMe); - }); - container - .querySelectorAll(".djDebugUncollapsed") - .forEach(function (e) { - $$.toggle(e, !openMe); - }); - const self = this; + container.querySelectorAll(".djDebugCollapsed").forEach((e) => { + $$.toggle(e, openMe); + }); + container.querySelectorAll(".djDebugUncollapsed").forEach((e) => { + $$.toggle(e, !openMe); + }); this.closest(".djDebugPanelContent") .querySelectorAll(`.djToggleDetails_${id}`) - .forEach(function (e) { + .forEach((e) => { if (openMe) { e.classList.add("djSelected"); e.classList.remove("djUnselected"); - self.textContent = toggleClose; + this.textContent = toggleClose; } else { e.classList.remove("djSelected"); e.classList.add("djUnselected"); - self.textContent = toggleOpen; + this.textContent = toggleOpen; } const switch_ = e.querySelector(".djToggleSwitch"); if (switch_) { - switch_.textContent = self.textContent; + switch_.textContent = this.textContent; } }); }); - $$.on(djDebug, "click", "#djHideToolBarButton", function (event) { + $$.on(djDebug, "click", "#djHideToolBarButton", (event) => { event.preventDefault(); djdt.hideToolbar(); }); - $$.on(djDebug, "click", "#djShowToolBarButton", function () { + $$.on(djDebug, "click", "#djShowToolBarButton", () => { if (!djdt.handleDragged) { djdt.showToolbar(); } @@ -177,7 +172,7 @@ const djdt = { djdt.handleDragged = true; } } - $$.on(djDebug, "mousedown", "#djShowToolBarButton", function (event) { + $$.on(djDebug, "mousedown", "#djShowToolBarButton", (event) => { event.preventDefault(); startPageY = event.pageY; baseY = handle.offsetTop - startPageY; @@ -185,12 +180,12 @@ const djdt = { document.addEventListener( "mouseup", - function (event) { + (event) => { document.removeEventListener("mousemove", onHandleMove); if (djdt.handleDragged) { event.preventDefault(); localStorage.setItem("djdt.top", handle.offsetTop); - requestAnimationFrame(function () { + requestAnimationFrame(() => { djdt.handleDragged = false; }); djdt.ensureHandleVisibility(); @@ -221,7 +216,7 @@ const djdt = { djDebug.setAttribute("data-theme", userTheme); } // Adds the listener to the Theme Toggle Button - $$.on(djDebug, "click", "#djToggleThemeButton", function () { + $$.on(djDebug, "click", "#djToggleThemeButton", () => { switch (djDebug.getAttribute("data-theme")) { case "auto": djDebug.setAttribute("data-theme", "light"); @@ -241,10 +236,10 @@ const djdt = { hidePanels() { const djDebug = getDebugElement(); $$.hide(document.getElementById("djDebugWindow")); - djDebug.querySelectorAll(".djdt-panelContent").forEach(function (e) { + djDebug.querySelectorAll(".djdt-panelContent").forEach((e) => { $$.hide(e); }); - document.querySelectorAll("#djDebugToolbar li").forEach(function (e) { + document.querySelectorAll("#djDebugToolbar li").forEach((e) => { e.classList.remove("djdt-active"); }); }, @@ -299,7 +294,7 @@ const djdt = { function handleAjaxResponse(storeId) { const encodedStoreId = encodeURIComponent(storeId); const dest = `${sidebarUrl}?store_id=${encodedStoreId}`; - slowjax(dest).then(function (data) { + slowjax(dest).then((data) => { if (djdt.needUpdateOnFetch) { replaceToolbarState(encodedStoreId, data); } @@ -325,7 +320,7 @@ const djdt = { const origFetch = window.fetch; window.fetch = function (...args) { const promise = origFetch.apply(this, args); - promise.then(function (response) { + promise.then((response) => { if (response.headers.get("djdt-store-id") !== null) { handleAjaxResponse(response.headers.get("djdt-store-id")); } @@ -345,7 +340,7 @@ const djdt = { const cookieArray = document.cookie.split("; "); const cookies = {}; - cookieArray.forEach(function (e) { + cookieArray.forEach((e) => { const parts = e.split("="); cookies[parts[0]] = parts[1]; }); diff --git a/debug_toolbar/static/debug_toolbar/js/utils.js b/debug_toolbar/static/debug_toolbar/js/utils.js index e2f03bb2b..0b46e6640 100644 --- a/debug_toolbar/static/debug_toolbar/js/utils.js +++ b/debug_toolbar/static/debug_toolbar/js/utils.js @@ -1,7 +1,7 @@ const $$ = { on(root, eventName, selector, fn) { root.removeEventListener(eventName, fn); - root.addEventListener(eventName, function (event) { + root.addEventListener(eventName, (event) => { const target = event.target.closest(selector); if (root.contains(target)) { fn.call(target, event); @@ -17,7 +17,7 @@ const $$ = { panelId: The Id of the panel. fn: A function to execute when the event is triggered. */ - root.addEventListener("djdt.panel.render", function (event) { + root.addEventListener("djdt.panel.render", (event) => { if (event.detail.panelId === panelId) { fn.call(event); } @@ -40,7 +40,7 @@ const $$ = { return !element.classList.contains("djdt-hidden"); }, executeScripts(scripts) { - scripts.forEach(function (script) { + scripts.forEach((script) => { const el = document.createElement("script"); el.type = "module"; el.src = script; @@ -54,39 +54,39 @@ const $$ = { * The format is data-djdt-styles="styleName1:value;styleName2:value2" * The style names should use the CSSStyleDeclaration camel cased names. */ - container - .querySelectorAll("[data-djdt-styles]") - .forEach(function (element) { - const styles = element.dataset.djdtStyles || ""; - styles.split(";").forEach(function (styleText) { - const styleKeyPair = styleText.split(":"); - if (styleKeyPair.length === 2) { - const name = styleKeyPair[0].trim(); - const value = styleKeyPair[1].trim(); - element.style[name] = value; - } - }); + container.querySelectorAll("[data-djdt-styles]").forEach((element) => { + const styles = element.dataset.djdtStyles || ""; + styles.split(";").forEach((styleText) => { + const styleKeyPair = styleText.split(":"); + if (styleKeyPair.length === 2) { + const name = styleKeyPair[0].trim(); + const value = styleKeyPair[1].trim(); + element.style[name] = value; + } }); + }); }, }; function ajax(url, init) { return fetch(url, Object.assign({ credentials: "same-origin" }, init)) - .then(function (response) { + .then((response) => { if (response.ok) { - return response.json().catch(function (error) { - return Promise.reject( - new Error( - `The response is a invalid Json object : ${error}` + return response + .json() + .catch((error) => + Promise.reject( + new Error( + `The response is a invalid Json object : ${error}` + ) ) ); - }); } return Promise.reject( new Error(`${response.status}: ${response.statusText}`) ); }) - .catch(function (error) { + .catch((error) => { const win = document.getElementById("djDebugWindow"); win.innerHTML = `

    ${error.message}

    `; $$.show(win); @@ -111,7 +111,7 @@ 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 storeId. - Object.keys(data).forEach(function (panelId) { + Object.keys(data).forEach((panelId) => { const panel = document.getElementById(panelId); if (panel) { panel.outerHTML = data[panelId].content; @@ -125,7 +125,7 @@ function debounce(func, delay) { let timer = null; let resolves = []; - return function (...args) { + return (...args) => { clearTimeout(timer); timer = setTimeout(() => { const result = func(...args); From 47bf0a29f5152589bc8a05e3d31174a72aba4e86 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Sat, 1 Mar 2025 16:20:24 +0100 Subject: [PATCH 164/200] Enable the noAssignInExpressions rule --- biome.json | 3 --- debug_toolbar/static/debug_toolbar/js/toolbar.js | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/biome.json b/biome.json index 7d0bdd912..b905286b3 100644 --- a/biome.json +++ b/biome.json @@ -13,9 +13,6 @@ "recommended": true, "complexity": { "noForEach": "off" - }, - "suspicious": { - "noAssignInExpressions": "off" } } }, diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 3e99e6ecb..08f4fc75c 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -350,8 +350,9 @@ const djdt = { set(key, value, options = {}) { if (typeof options.expires === "number") { const days = options.expires; - const t = (options.expires = new Date()); - t.setDate(t.getDate() + days); + const expires = new Date(); + expires.setDate(expires.setDate() + days); + options.expires = expires; } document.cookie = [ From 262f6e54fdc0b5ec3fc4b4554424e0ef0da8ea96 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Sat, 1 Mar 2025 16:28:16 +0100 Subject: [PATCH 165/200] Replace forEach loops with for...of loops --- biome.json | 5 +- .../static/debug_toolbar/js/history.js | 30 ++++----- .../static/debug_toolbar/js/toolbar.js | 64 ++++++++++--------- .../static/debug_toolbar/js/utils.js | 22 ++++--- 4 files changed, 61 insertions(+), 60 deletions(-) diff --git a/biome.json b/biome.json index b905286b3..625e4ebe7 100644 --- a/biome.json +++ b/biome.json @@ -10,10 +10,7 @@ "linter": { "enabled": true, "rules": { - "recommended": true, - "complexity": { - "noForEach": "off" - } + "recommended": true } }, "javascript": { diff --git a/debug_toolbar/static/debug_toolbar/js/history.js b/debug_toolbar/static/debug_toolbar/js/history.js index a0d339f2b..d10156660 100644 --- a/debug_toolbar/static/debug_toolbar/js/history.js +++ b/debug_toolbar/static/debug_toolbar/js/history.js @@ -14,11 +14,7 @@ function difference(setA, setB) { * Create an array of dataset properties from a NodeList. */ function pluckData(nodes, key) { - const data = []; - nodes.forEach((obj) => { - data.push(obj.dataset[key]); - }); - return data; + return [...nodes].map((obj) => obj.dataset[key]); } function refreshHistory() { @@ -31,12 +27,14 @@ function refreshHistory() { ajaxForm(formTarget) .then((data) => { // Remove existing rows first then re-populate with new data - container.querySelectorAll("tr[data-store-id]").forEach((node) => { + for (const node of container.querySelectorAll( + "tr[data-store-id]" + )) { node.remove(); - }); - data.requests.forEach((request) => { + } + for (const request of data.requests) { container.innerHTML = request.content + container.innerHTML; - }); + } }) .then(() => { const allIds = new Set( @@ -54,18 +52,18 @@ function refreshHistory() { }; }) .then((refreshInfo) => { - refreshInfo.newIds.forEach((newId) => { + for (const newId of refreshInfo.newIds) { const row = container.querySelector( `tr[data-store-id="${newId}"]` ); row.classList.add("flash-new"); - }); + } setTimeout(() => { - container - .querySelectorAll("tr[data-store-id]") - .forEach((row) => { - row.classList.remove("flash-new"); - }); + for (const row of container.querySelectorAll( + "tr[data-store-id]" + )) { + row.classList.remove("flash-new"); + } }, 2000); }); } diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 08f4fc75c..329bce669 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -117,29 +117,31 @@ const djdt = { const openMe = this.textContent === toggleOpen; const name = this.dataset.toggleName; const container = document.getElementById(`${name}_${id}`); - container.querySelectorAll(".djDebugCollapsed").forEach((e) => { - $$.toggle(e, openMe); - }); - container.querySelectorAll(".djDebugUncollapsed").forEach((e) => { - $$.toggle(e, !openMe); - }); - this.closest(".djDebugPanelContent") - .querySelectorAll(`.djToggleDetails_${id}`) - .forEach((e) => { - if (openMe) { - e.classList.add("djSelected"); - e.classList.remove("djUnselected"); - this.textContent = toggleClose; - } else { - e.classList.remove("djSelected"); - e.classList.add("djUnselected"); - this.textContent = toggleOpen; - } - const switch_ = e.querySelector(".djToggleSwitch"); - if (switch_) { - switch_.textContent = this.textContent; - } - }); + for (const el of container.querySelectorAll(".djDebugCollapsed")) { + $$.toggle(el, openMe); + } + for (const el of container.querySelectorAll( + ".djDebugUncollapsed" + )) { + $$.toggle(el, !openMe); + } + for (const el of this.closest( + ".djDebugPanelContent" + ).querySelectorAll(`.djToggleDetails_${id}`)) { + if (openMe) { + el.classList.add("djSelected"); + el.classList.remove("djUnselected"); + this.textContent = toggleClose; + } else { + el.classList.remove("djSelected"); + el.classList.add("djUnselected"); + this.textContent = toggleOpen; + } + const switch_ = el.querySelector(".djToggleSwitch"); + if (switch_) { + switch_.textContent = this.textContent; + } + } }); $$.on(djDebug, "click", "#djHideToolBarButton", (event) => { @@ -236,12 +238,12 @@ const djdt = { hidePanels() { const djDebug = getDebugElement(); $$.hide(document.getElementById("djDebugWindow")); - djDebug.querySelectorAll(".djdt-panelContent").forEach((e) => { - $$.hide(e); - }); - document.querySelectorAll("#djDebugToolbar li").forEach((e) => { - e.classList.remove("djdt-active"); - }); + for (const el of djDebug.querySelectorAll(".djdt-panelContent")) { + $$.hide(el); + } + for (const el of document.querySelectorAll("#djDebugToolbar li")) { + el.classList.remove("djdt-active"); + } }, ensureHandleVisibility() { const handle = document.getElementById("djDebugToolbarHandle"); @@ -340,10 +342,10 @@ const djdt = { const cookieArray = document.cookie.split("; "); const cookies = {}; - cookieArray.forEach((e) => { + for (const e of cookieArray) { const parts = e.split("="); cookies[parts[0]] = parts[1]; - }); + } return cookies[key]; }, diff --git a/debug_toolbar/static/debug_toolbar/js/utils.js b/debug_toolbar/static/debug_toolbar/js/utils.js index 0b46e6640..c42963fe3 100644 --- a/debug_toolbar/static/debug_toolbar/js/utils.js +++ b/debug_toolbar/static/debug_toolbar/js/utils.js @@ -40,13 +40,13 @@ const $$ = { return !element.classList.contains("djdt-hidden"); }, executeScripts(scripts) { - scripts.forEach((script) => { + for (const script of scripts) { const el = document.createElement("script"); el.type = "module"; el.src = script; el.async = true; document.head.appendChild(el); - }); + } }, applyStyles(container) { /* @@ -54,17 +54,19 @@ const $$ = { * The format is data-djdt-styles="styleName1:value;styleName2:value2" * The style names should use the CSSStyleDeclaration camel cased names. */ - container.querySelectorAll("[data-djdt-styles]").forEach((element) => { + for (const element of container.querySelectorAll( + "[data-djdt-styles]" + )) { const styles = element.dataset.djdtStyles || ""; - styles.split(";").forEach((styleText) => { + for (const styleText of styles.split(";")) { const styleKeyPair = styleText.split(":"); if (styleKeyPair.length === 2) { const name = styleKeyPair[0].trim(); const value = styleKeyPair[1].trim(); element.style[name] = value; } - }); - }); + } + } }, }; @@ -111,14 +113,14 @@ 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 storeId. - Object.keys(data).forEach((panelId) => { + for (const panelId of Object.keys(data)) { const panel = document.getElementById(panelId); if (panel) { panel.outerHTML = data[panelId].content; document.getElementById(`djdt-${panelId}`).outerHTML = data[panelId].button; } - }); + } } function debounce(func, delay) { @@ -129,7 +131,9 @@ function debounce(func, delay) { clearTimeout(timer); timer = setTimeout(() => { const result = func(...args); - resolves.forEach((r) => r(result)); + for (const r of resolves) { + r(result); + } resolves = []; }, delay); From b78fd4d12c0fae0fbade1d6f5bce318081d00c2a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 08:17:05 +0100 Subject: [PATCH 166/200] [pre-commit.ci] pre-commit autoupdate (#2095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.7 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.7...v0.9.9) - [github.com/tox-dev/pyproject-fmt: v2.5.0 → v2.5.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.5.0...v2.5.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9317b643..adf0aed43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,13 +29,13 @@ repos: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.9.7' + rev: 'v0.9.9' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.0 + rev: v2.5.1 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject From b5dc19c94613e28d9a472e7bc7ad088ae875f597 Mon Sep 17 00:00:00 2001 From: Abdulwasiu Apalowo <64538336+mrbazzan@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:35:43 +0100 Subject: [PATCH 167/200] Add help command to the Makefile (#2094) Closes #2092 --- Makefile | 23 ++++++++++++++--------- docs/changes.rst | 2 ++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 24b59ab95..4d2db27af 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,24 @@ -.PHONY: example test coverage translatable_strings update_translations +.PHONY: example test coverage translatable_strings update_translations help +.DEFAULT_GOAL := help -example: +example: ## Run the example application python example/manage.py migrate --noinput -DJANGO_SUPERUSER_PASSWORD=p python example/manage.py createsuperuser \ --noinput --username="$(USER)" --email="$(USER)@mailinator.com" python example/manage.py runserver -example_test: +example_test: ## Run the test suite for the example application python example/manage.py test example -test: +test: ## Run the test suite DJANGO_SETTINGS_MODULE=tests.settings \ python -m django test $${TEST_ARGS:-tests} -test_selenium: +test_selenium: ## Run frontend tests written with Selenium DJANGO_SELENIUM_TESTS=true DJANGO_SETTINGS_MODULE=tests.settings \ python -m django test $${TEST_ARGS:-tests} -coverage: +coverage: ## Run the test suite with coverage enabled python --version DJANGO_SETTINGS_MODULE=tests.settings \ python -b -W always -m coverage run -m django test -v2 $${TEST_ARGS:-tests} @@ -25,15 +26,19 @@ coverage: coverage html coverage xml -translatable_strings: +translatable_strings: ## Update the English '.po' file cd debug_toolbar && python -m django makemessages -l en --no-obsolete @echo "Please commit changes and run 'tx push -s' (or wait for Transifex to pick them)" -update_translations: +update_translations: ## Download updated '.po' files from Transifex tx pull -a --minimum-perc=10 cd debug_toolbar && python -m django compilemessages .PHONY: example/django-debug-toolbar.png -example/django-debug-toolbar.png: example/screenshot.py +example/django-debug-toolbar.png: example/screenshot.py ## Update the screenshot in 'README.rst' python $< --browser firefox --headless -o $@ optipng $@ + +help: ## Help message for targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/docs/changes.rst b/docs/changes.rst index b75e0daf7..8ca86692d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,8 @@ Pending * Make ``require_toolbar`` decorator compatible to async views. * Added link to contributing documentation in ``CONTRIBUTING.md``. * Replaced ESLint and prettier with biome in our pre-commit configuration. +* Added a Makefile target (``make help``) to get a quick overview + of each target. 5.0.1 (2025-01-13) ------------------ From e885fe92ea2b984fca37403d38f7fc7cdb20f06a Mon Sep 17 00:00:00 2001 From: Felipe Villegas Date: Wed, 5 Mar 2025 10:52:23 -0500 Subject: [PATCH 168/200] Replace DebugConfiguredStorage with URLMixin in staticfiles panel (#2097) * Refs #2068: Do not reinstantiate staticfiles storage classes * Add URLMixin tests for staticfiles panel - Test storage state preservation to ensure URLMixin doesn't affect storage attributes - Test context variable lifecycle for static file tracking - Test multiple initialization safety to prevent URLMixin stacking * Update changelog: added URLMixin * Add words to the spelling wordlist Co-authored-by: Matthias Kestenholz --- debug_toolbar/panels/staticfiles.py | 61 +++++++++-------------------- docs/changes.rst | 1 + docs/spelling_wordlist.txt | 3 ++ tests/panels/test_staticfiles.py | 45 ++++++++++++++++++++- 4 files changed, 66 insertions(+), 44 deletions(-) diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py index 3dd29e979..9f1970ef6 100644 --- a/debug_toolbar/panels/staticfiles.py +++ b/debug_toolbar/panels/staticfiles.py @@ -3,10 +3,8 @@ from contextvars import ContextVar from os.path import join, normpath -from django.conf import settings from django.contrib.staticfiles import finders, storage from django.dispatch import Signal -from django.utils.functional import LazyObject from django.utils.translation import gettext_lazy as _, ngettext from debug_toolbar import panels @@ -37,46 +35,21 @@ def url(self): record_static_file_signal = Signal() -class DebugConfiguredStorage(LazyObject): - """ - A staticfiles storage class to be used for collecting which paths - are resolved by using the {% static %} template tag (which uses the - `url` method). - """ - - def _setup(self): - try: - # From Django 4.2 use django.core.files.storage.storages in favor - # of the deprecated django.core.files.storage.get_storage_class - from django.core.files.storage import storages - - configured_storage_cls = storages["staticfiles"].__class__ - except ImportError: - # Backwards compatibility for Django versions prior to 4.2 - from django.core.files.storage import get_storage_class - - configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE) - - class DebugStaticFilesStorage(configured_storage_cls): - def url(self, path): - url = super().url(path) - with contextlib.suppress(LookupError): - # For LookupError: - # The ContextVar wasn't set yet. Since the toolbar wasn't properly - # configured to handle this request, we don't need to capture - # the static file. - request_id = request_id_context_var.get() - record_static_file_signal.send( - sender=self, - staticfile=StaticFile(path=str(path), url=url), - request_id=request_id, - ) - return url - - self._wrapped = DebugStaticFilesStorage() - - -_original_storage = storage.staticfiles_storage +class URLMixin: + def url(self, path): + url = super().url(path) + with contextlib.suppress(LookupError): + # For LookupError: + # The ContextVar wasn't set yet. Since the toolbar wasn't properly + # configured to handle this request, we don't need to capture + # the static file. + request_id = request_id_context_var.get() + record_static_file_signal.send( + sender=self, + staticfile=StaticFile(path=str(path), url=url), + request_id=request_id, + ) + return url class StaticFilesPanel(panels.Panel): @@ -103,7 +76,9 @@ def __init__(self, *args, **kwargs): @classmethod def ready(cls): - storage.staticfiles_storage = DebugConfiguredStorage() + cls = storage.staticfiles_storage.__class__ + if URLMixin not in cls.mro(): + cls.__bases__ = (URLMixin, *cls.__bases__) def _store_static_files_signal_handler(self, sender, staticfile, **kwargs): # Only record the static file if the request_id matches the one diff --git a/docs/changes.rst b/docs/changes.rst index 8ca86692d..608843e0f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -14,6 +14,7 @@ Pending * Replaced ESLint and prettier with biome in our pre-commit configuration. * Added a Makefile target (``make help``) to get a quick overview of each target. +* Avoided reinitializing the staticfiles storage during instrumentation. 5.0.1 (2025-01-13) ------------------ diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 662e6df4f..8db8072b7 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -13,6 +13,7 @@ async backend backends backported +biome checkbox contrib dicts @@ -50,6 +51,7 @@ pylibmc pyupgrade querysets refactoring +reinitializing resizing runserver spellchecking @@ -57,6 +59,7 @@ spooler stacktrace stacktraces startup +staticfiles theming timeline tox diff --git a/tests/panels/test_staticfiles.py b/tests/panels/test_staticfiles.py index 334b0b6a3..2306c8365 100644 --- a/tests/panels/test_staticfiles.py +++ b/tests/panels/test_staticfiles.py @@ -1,10 +1,12 @@ from pathlib import Path from django.conf import settings -from django.contrib.staticfiles import finders +from django.contrib.staticfiles import finders, storage from django.shortcuts import render from django.test import AsyncRequestFactory, RequestFactory +from debug_toolbar.panels.staticfiles import URLMixin + from ..base import BaseTestCase @@ -76,3 +78,44 @@ def get_response(request): self.panel.generate_stats(self.request, response) self.assertEqual(self.panel.num_used, 1) self.assertIn('"/static/additional_static/base.css"', self.panel.content) + + def test_storage_state_preservation(self): + """Ensure the URLMixin doesn't affect storage state""" + original_storage = storage.staticfiles_storage + original_attrs = dict(original_storage.__dict__) + + # Trigger mixin injection + self.panel.ready() + + # Verify all original attributes are preserved + self.assertEqual(original_attrs, dict(original_storage.__dict__)) + + def test_context_variable_lifecycle(self): + """Test the request_id context variable lifecycle""" + from debug_toolbar.panels.staticfiles import request_id_context_var + + # Should not raise when context not set + url = storage.staticfiles_storage.url("test.css") + self.assertTrue(url.startswith("/static/")) + + # Should track when context is set + token = request_id_context_var.set("test-request-id") + try: + url = storage.staticfiles_storage.url("test.css") + self.assertTrue(url.startswith("/static/")) + # Verify file was tracked + self.assertIn("test.css", [f.path for f in self.panel.used_paths]) + finally: + request_id_context_var.reset(token) + + def test_multiple_initialization(self): + """Ensure multiple panel initializations don't stack URLMixin""" + storage_class = storage.staticfiles_storage.__class__ + + # Initialize panel multiple times + for _ in range(3): + self.panel.ready() + + # Verify URLMixin appears exactly once in bases + mixin_count = sum(1 for base in storage_class.__bases__ if base == URLMixin) + self.assertEqual(mixin_count, 1) From 42696c2e9534674af754ba84d244743509137ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20K=C3=A1rolyi?= <987055+karolyi@users.noreply.github.com> Date: Mon, 10 Mar 2025 13:23:16 +0000 Subject: [PATCH 169/200] Fix for exception-unhandled "forked" Promise chain (#2101) See https://github.com/django-commons/django-debug-toolbar/pull/2100 Co-authored-by: Tim Schilling --- .../static/debug_toolbar/js/toolbar.js | 19 ++++++++++++++----- docs/changes.rst | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 329bce669..077bc930a 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -321,16 +321,25 @@ const djdt = { const origFetch = window.fetch; 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); - promise.then((response) => { + return promise.then((response) => { if (response.headers.get("djdt-store-id") !== null) { - handleAjaxResponse(response.headers.get("djdt-store-id")); + try { + handleAjaxResponse( + response.headers.get("djdt-store-id") + ); + } catch (err) { + throw new Error( + `"${err.name}" occurred within django-debug-toolbar: ${err.message}` + ); + } } - // Don't resolve the response via .json(). Instead - // continue to return it to allow the caller to consume as needed. return response; }); - return promise; }; }, cookie: { diff --git a/docs/changes.rst b/docs/changes.rst index 608843e0f..f11d4889e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -15,6 +15,7 @@ Pending * Added a Makefile target (``make help``) to get a quick overview of each target. * Avoided reinitializing the staticfiles storage during instrumentation. +* Fix for exception-unhandled "forked" Promise chain in rebound window.fetch 5.0.1 (2025-01-13) ------------------ From 240140646c539f0699694fdf65a205d9db19acc6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:13:25 +0100 Subject: [PATCH 170/200] [pre-commit.ci] pre-commit autoupdate (#2102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.9.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.9.10) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index adf0aed43..7dad19ea1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.9.9' + rev: 'v0.9.10' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 86b67bb7242766ff5b51e6a0b1fb8c4af88f6150 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Wed, 19 Mar 2025 10:39:00 +0100 Subject: [PATCH 171/200] Reword a changelog entry --- docs/changes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index f11d4889e..4337ec516 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -15,7 +15,8 @@ Pending * Added a Makefile target (``make help``) to get a quick overview of each target. * Avoided reinitializing the staticfiles storage during instrumentation. -* Fix for exception-unhandled "forked" Promise chain in rebound window.fetch +* Avoided a "forked" Promise chain in the rebound ``window.fetch`` function + with missing exception handling. 5.0.1 (2025-01-13) ------------------ From 0d9b80eec040d6bd2bc23d8fca919862af55ec52 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:39:55 +0100 Subject: [PATCH 172/200] [pre-commit.ci] pre-commit autoupdate (#2107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.10 → v0.11.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.10...v0.11.0) - [github.com/abravalheri/validate-pyproject: v0.23 → v0.24](https://github.com/abravalheri/validate-pyproject/compare/v0.23...v0.24) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- debug_toolbar/panels/cache.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7dad19ea1..ee54d2d5d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.9.10' + rev: 'v0.11.0' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -39,6 +39,6 @@ repos: hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.23 + rev: v0.24 hooks: - id: validate-pyproject diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py index 0f8902b5a..1b15b446f 100644 --- a/debug_toolbar/panels/cache.py +++ b/debug_toolbar/panels/cache.py @@ -68,7 +68,7 @@ def __init__(self, *args, **kwargs): self.hits = 0 self.misses = 0 self.calls = [] - self.counts = {name: 0 for name in WRAPPED_CACHE_METHODS} + self.counts = dict.fromkeys(WRAPPED_CACHE_METHODS, 0) @classmethod def current_instance(cls): From c557f2473949d432e0471a36faefd0cfaf913dc5 Mon Sep 17 00:00:00 2001 From: Prashant Andoriya <121665385+andoriyaprashant@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:15:21 +0530 Subject: [PATCH 173/200] Fix Dark Mode Conflict in Pygments (#2108) --- debug_toolbar/static/debug_toolbar/css/toolbar.css | 2 +- docs/changes.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index 47f4abb2d..3d0d34e6c 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -37,7 +37,7 @@ @media (prefers-color-scheme: dark) { :root { - --djdt-font-color: #8393a7; + --djdt-font-color: #f8f8f2; --djdt-background-color: #1e293bff; --djdt-panel-content-background-color: #0f1729ff; --djdt-panel-title-background-color: #242432; diff --git a/docs/changes.rst b/docs/changes.rst index 4337ec516..0a13dc4b3 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -17,6 +17,7 @@ Pending * Avoided reinitializing the staticfiles storage during instrumentation. * Avoided a "forked" Promise chain in the rebound ``window.fetch`` function with missing exception handling. +* Fixed the pygments code highlighting when using dark mode. 5.0.1 (2025-01-13) ------------------ From cbb479fadf0c0ffa725c98b9d663b0c058f4b71d Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Wed, 19 Mar 2025 13:44:16 -0500 Subject: [PATCH 174/200] Refactor on csp_nonce usage with django-csp (#2088) * Consolidate csp_nonce usages to a single property on the toolbar. This refactors how the CSP nonce is fetched. It's now done as a toolbar property and wraps the attribute request.csp_nonce * Add csp to our words list. * Unpin django-csp for tests. --- .../templates/debug_toolbar/base.html | 6 +- .../debug_toolbar/includes/panel_content.html | 2 +- .../templates/debug_toolbar/redirect.html | 2 +- debug_toolbar/toolbar.py | 10 ++ docs/changes.rst | 2 + docs/spelling_wordlist.txt | 1 + tests/test_csp_rendering.py | 144 +++++++++++------- tests/urls.py | 1 + tests/views.py | 5 + tox.ini | 2 +- 10 files changed, 114 insertions(+), 61 deletions(-) diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index b0308be55..a9983250d 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -1,10 +1,10 @@ {% load i18n static %} {% block css %} - - + + {% endblock %} {% block js %} - + {% endblock %}

    {{ panel.title }}

    {% if toolbar.should_render_panels %} - {% for script in panel.scripts %}{% endfor %} + {% for script in panel.scripts %}{% endfor %}
    {{ panel.content }}
    {% else %}
    diff --git a/debug_toolbar/templates/debug_toolbar/redirect.html b/debug_toolbar/templates/debug_toolbar/redirect.html index cb6b4a6ea..9d8966ed7 100644 --- a/debug_toolbar/templates/debug_toolbar/redirect.html +++ b/debug_toolbar/templates/debug_toolbar/redirect.html @@ -3,7 +3,7 @@ Django Debug Toolbar Redirects Panel: {{ status_line }} - +

    {{ status_line }}

    diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py index afb7affac..04e5894c5 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -65,6 +65,16 @@ def enabled_panels(self): """ return [panel for panel in self._panels.values() if panel.enabled] + @property + def csp_nonce(self): + """ + Look up the Content Security Policy nonce if there is one. + + This is built specifically for django-csp, which may not always + have a nonce associated with the request. + """ + return getattr(self.request, "csp_nonce", None) + def get_panel_by_id(self, panel_id): """ Get the panel with the given id, which is the class name by default. diff --git a/docs/changes.rst b/docs/changes.rst index 0a13dc4b3..572694eb7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,6 +18,8 @@ Pending * Avoided a "forked" Promise chain in the rebound ``window.fetch`` function with missing exception handling. * Fixed the pygments code highlighting when using dark mode. +* Fix for exception-unhandled "forked" Promise chain in rebound window.fetch +* Create a CSP nonce property on the toolbar ``Toolbar().csp_nonce``. 5.0.1 (2025-01-13) ------------------ diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 8db8072b7..0f58c1f52 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -16,6 +16,7 @@ backported biome checkbox contrib +csp dicts django fallbacks diff --git a/tests/test_csp_rendering.py b/tests/test_csp_rendering.py index a84f958c1..144e65ba0 100644 --- a/tests/test_csp_rendering.py +++ b/tests/test_csp_rendering.py @@ -13,6 +13,13 @@ from .base import IntegrationTestCase +MIDDLEWARE_CSP_BEFORE = settings.MIDDLEWARE.copy() +MIDDLEWARE_CSP_BEFORE.insert( + MIDDLEWARE_CSP_BEFORE.index("debug_toolbar.middleware.DebugToolbarMiddleware"), + "csp.middleware.CSPMiddleware", +) +MIDDLEWARE_CSP_LAST = settings.MIDDLEWARE + ["csp.middleware.CSPMiddleware"] + def get_namespaces(element: Element) -> dict[str, str]: """ @@ -63,70 +70,97 @@ def _fail_on_invalid_html(self, content: bytes, parser: HTMLParser): msg = self._formatMessage(None, "\n".join(default_msg)) raise self.failureException(msg) - @override_settings( - MIDDLEWARE=settings.MIDDLEWARE + ["csp.middleware.CSPMiddleware"] - ) def test_exists(self): """A `nonce` should exist when using the `CSPMiddleware`.""" - 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) - toolbar = list(DebugToolbar._store.values())[0] - nonce = str(toolbar.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 - ) + 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) + + 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) + toolbar = list(DebugToolbar._store.values())[-1] + 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_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()}, - MIDDLEWARE=settings.MIDDLEWARE + ["csp.middleware.CSPMiddleware"], ) def test_redirects_exists(self): - response = cast(HttpResponse, self.client.get(path="/regular/basic/")) - self.assertEqual(response.status_code, 200) + 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) + + 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) + context: ContextList = response.context # pyright: ignore[reportAttributeAccessIssue] + nonce = str(context["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 + ) - 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) - context: ContextList = response.context # pyright: ignore[reportAttributeAccessIssue] - nonce = str(context["toolbar"].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 - ) - - @override_settings( - MIDDLEWARE=settings.MIDDLEWARE + ["csp.middleware.CSPMiddleware"] - ) def test_panel_content_nonce_exists(self): - response = cast(HttpResponse, self.client.get(path="/regular/basic/")) - self.assertEqual(response.status_code, 200) - - toolbar = list(DebugToolbar._store.values())[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) - nonce = str(toolbar.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 - ) + 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) + + toolbar = list(DebugToolbar._store.values())[-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`.""" diff --git a/tests/urls.py b/tests/urls.py index 68c6e0354..124e55892 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -25,6 +25,7 @@ path("redirect/", views.redirect_view), path("ajax/", views.ajax_view), path("login_without_redirect/", LoginView.as_view(redirect_field_name=None)), + path("csp_view/", views.csp_view), path("admin/", admin.site.urls), path("__debug__/", include("debug_toolbar.urls")), ] diff --git a/tests/views.py b/tests/views.py index e8528ff2e..b6e3252af 100644 --- a/tests/views.py +++ b/tests/views.py @@ -42,6 +42,11 @@ def regular_view(request, title): return render(request, "basic.html", {"title": title}) +def csp_view(request): + """Use request.csp_nonce to inject it into the headers""" + return render(request, "basic.html", {"title": f"CSP {request.csp_nonce}"}) + + def template_response_view(request, title): return TemplateResponse(request, "basic.html", {"title": title}) diff --git a/tox.ini b/tox.ini index 691ba2670..c8f4a6815 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ deps = pygments selenium>=4.8.0 sqlparse - django-csp<4 + django-csp passenv= CI COVERAGE_ARGS From 0d7b85940e8142a5394c0f28d635b019232c4b67 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Thu, 20 Mar 2025 16:58:41 +0100 Subject: [PATCH 175/200] Version 5.1.0 --- README.rst | 2 +- debug_toolbar/__init__.py | 2 +- docs/changes.rst | 2 ++ docs/conf.py | 2 +- example/django-debug-toolbar.png | Bin 84160 -> 76428 bytes pyproject.toml | 1 + 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 99b127526..3c831efa7 100644 --- a/README.rst +++ b/README.rst @@ -40,7 +40,7 @@ Here's a screenshot of the toolbar in action: In addition to the built-in panels, a number of third-party panels are contributed by the community. -The current stable version of the Debug Toolbar is 5.0.1. It works on +The current stable version of the Debug Toolbar is 5.1.0. It works on Django ≥ 4.2.0. The Debug Toolbar has experimental support for `Django's asynchronous views diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py index 2180a5880..5bdaa2dd1 100644 --- a/debug_toolbar/__init__.py +++ b/debug_toolbar/__init__.py @@ -4,7 +4,7 @@ # Do not use pkg_resources to find the version but set it here directly! # see issue #1446 -VERSION = "5.0.1" +VERSION = "5.1.0" # Code that discovers files or modules in INSTALLED_APPS imports this module. urls = "debug_toolbar.urls", APP_NAME diff --git a/docs/changes.rst b/docs/changes.rst index 572694eb7..dd52a09e1 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,8 @@ Change log Pending ------- +5.1.0 (2025-03-20) +------------------ * Added Django 5.2 to the tox matrix. * Updated package metadata to include well-known labels. * Added resources section to the documentation. diff --git a/docs/conf.py b/docs/conf.py index c8a6a5cea..4cb37988e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ copyright = copyright.format(datetime.date.today().year) # The full version, including alpha/beta/rc tags -release = "5.0.1" +release = "5.1.0" # -- General configuration --------------------------------------------------- diff --git a/example/django-debug-toolbar.png b/example/django-debug-toolbar.png index 414df59e0b90c98ae6da057e2152da0c47d5c20e..e074973e68c7de631181097e7229c54d3db7a66a 100644 GIT binary patch literal 76428 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
  • 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 z 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 A(f|Me diff --git a/pyproject.toml b/pyproject.toml index d31fba500..adba4bb40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", From 092411630a843eb4559a2d8bb46398864e069418 Mon Sep 17 00:00:00 2001 From: Luna <60090391+blingblin-g@users.noreply.github.com> Date: Fri, 21 Mar 2025 06:28:08 +0900 Subject: [PATCH 176/200] Enhance RedirectsPanel with customizable redirect response hook (#2104) * Add interception_response hook * Fix comments for SimpleTemplateResponse * Docs for Custom RedirectPanel * Moved the changelog entry to the correct place. --- debug_toolbar/panels/redirects.py | 35 ++++++++++++++++++------------- docs/changes.rst | 2 ++ docs/panels.rst | 5 +++++ tests/panels/test_redirects.py | 13 ++++++++++++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/debug_toolbar/panels/redirects.py b/debug_toolbar/panels/redirects.py index 27cce4c17..8055c67ad 100644 --- a/debug_toolbar/panels/redirects.py +++ b/debug_toolbar/panels/redirects.py @@ -22,20 +22,8 @@ def _process_response(self, response): Common response processing logic. """ if 300 <= response.status_code < 400: - redirect_to = response.get("Location") - if redirect_to: - status_line = f"{response.status_code} {response.reason_phrase}" - cookies = response.cookies - context = { - "redirect_to": redirect_to, - "status_line": status_line, - "toolbar": self.toolbar, - } - # Using SimpleTemplateResponse avoids running global context processors. - response = SimpleTemplateResponse( - "debug_toolbar/redirect.html", context - ) - response.cookies = cookies + if redirect_to := response.get("Location"): + response = self.get_interception_response(response, redirect_to) response.render() return response @@ -53,3 +41,22 @@ def process_request(self, request): if iscoroutine(response): return self.aprocess_request(request, response) return self._process_response(response) + + def get_interception_response(self, response, redirect_to): + """ + Hook method to allow subclasses to customize the interception response. + """ + status_line = f"{response.status_code} {response.reason_phrase}" + cookies = response.cookies + original_response = response + context = { + "redirect_to": redirect_to, + "status_line": status_line, + "toolbar": self.toolbar, + "original_response": original_response, + } + # Using SimpleTemplateResponse avoids running global context processors. + response = SimpleTemplateResponse("debug_toolbar/redirect.html", context) + response.cookies = cookies + response.original_response = original_response + return response diff --git a/docs/changes.rst b/docs/changes.rst index dd52a09e1..5be81b2cb 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,8 @@ Change log Pending ------- +* Added hook to RedirectsPanel for subclass customization. + 5.1.0 (2025-03-20) ------------------ * Added Django 5.2 to the tox matrix. diff --git a/docs/panels.rst b/docs/panels.rst index be481fb6e..a116bff1e 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -122,6 +122,11 @@ Since this behavior is annoying when you aren't debugging a redirect, this panel is included but inactive by default. You can activate it by default with the ``DISABLE_PANELS`` configuration option. +To further customize the behavior, you can subclass the ``RedirectsPanel`` +and override the ``get_interception_response`` method to manipulate the +response directly. To use a custom ``RedirectsPanel``, you need to replace +the original one in ``DEBUG_TOOLBAR_PANELS`` in your ``settings.py``. + .. _profiling-panel: Profiling diff --git a/tests/panels/test_redirects.py b/tests/panels/test_redirects.py index 2abed9fd0..7d6d5ac06 100644 --- a/tests/panels/test_redirects.py +++ b/tests/panels/test_redirects.py @@ -85,3 +85,16 @@ async def get_response(request): response = await self.panel.process_request(self.request) self.assertIsInstance(response, HttpResponse) self.assertTrue(response is await_response) + + def test_original_response_preserved(self): + redirect = HttpResponse(status=302) + redirect["Location"] = "http://somewhere/else/" + self._get_response = lambda request: redirect + response = self.panel.process_request(self.request) + self.assertFalse(response is redirect) + self.assertTrue(hasattr(response, "original_response")) + self.assertTrue(response.original_response is redirect) + self.assertIsNone(response.get("Location")) + self.assertEqual( + response.original_response.get("Location"), "http://somewhere/else/" + ) From e6076b5007cd753c0106aad2a4eac0557197b149 Mon Sep 17 00:00:00 2001 From: Felipe Villegas Date: Thu, 20 Mar 2025 17:34:48 -0400 Subject: [PATCH 177/200] Sanitize sensitive variables in RequestPanel (#2105) --- debug_toolbar/panels/request.py | 20 +++------ debug_toolbar/utils.py | 46 +++++++++++++++++---- docs/changes.rst | 1 + tests/panels/test_request.py | 73 +++++++++++++++++++++++++++++++++ tests/test_utils.py | 62 ++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 21 deletions(-) diff --git a/debug_toolbar/panels/request.py b/debug_toolbar/panels/request.py index b77788637..5a24d6179 100644 --- a/debug_toolbar/panels/request.py +++ b/debug_toolbar/panels/request.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from debug_toolbar.panels import Panel -from debug_toolbar.utils import get_name_from_obj, get_sorted_request_variable +from debug_toolbar.utils import get_name_from_obj, sanitize_and_sort_request_vars class RequestPanel(Panel): @@ -26,9 +26,9 @@ def nav_subtitle(self): def generate_stats(self, request, response): self.record_stats( { - "get": get_sorted_request_variable(request.GET), - "post": get_sorted_request_variable(request.POST), - "cookies": get_sorted_request_variable(request.COOKIES), + "get": sanitize_and_sort_request_vars(request.GET), + "post": sanitize_and_sort_request_vars(request.POST), + "cookies": sanitize_and_sort_request_vars(request.COOKIES), } ) @@ -59,13 +59,5 @@ def generate_stats(self, request, response): self.record_stats(view_info) if hasattr(request, "session"): - try: - session_list = [ - (k, request.session.get(k)) for k in sorted(request.session.keys()) - ] - except TypeError: - session_list = [ - (k, request.session.get(k)) - for k in request.session.keys() # (it's not a dict) - ] - self.record_stats({"session": {"list": session_list}}) + session_data = dict(request.session) + self.record_stats({"session": sanitize_and_sort_request_vars(session_data)}) diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py index dc3cc1adc..f4b3eac38 100644 --- a/debug_toolbar/utils.py +++ b/debug_toolbar/utils.py @@ -14,10 +14,12 @@ from django.template import Node from django.utils.html import format_html from django.utils.safestring import SafeString, mark_safe +from django.views.debug import get_default_exception_reporter_filter from debug_toolbar import _stubs as stubs, settings as dt_settings _local_data = Local() +safe_filter = get_default_exception_reporter_filter() def _is_excluded_frame(frame: Any, excluded_modules: Sequence[str] | None) -> bool: @@ -215,20 +217,50 @@ def getframeinfo(frame: Any, context: int = 1) -> inspect.Traceback: return inspect.Traceback(filename, lineno, frame.f_code.co_name, lines, index) -def get_sorted_request_variable( +def sanitize_and_sort_request_vars( variable: dict[str, Any] | QueryDict, ) -> dict[str, list[tuple[str, Any]] | Any]: """ Get a data structure for showing a sorted list of variables from the - request data. + request data with sensitive values redacted. """ + if not isinstance(variable, (dict, QueryDict)): + return {"raw": variable} + + # Get sorted keys if possible, otherwise just list them + keys = _get_sorted_keys(variable) + + # Process the variable based on its type + if isinstance(variable, QueryDict): + result = _process_query_dict(variable, keys) + else: + result = _process_dict(variable, keys) + + return {"list": result} + + +def _get_sorted_keys(variable): + """Helper function to get sorted keys if possible.""" try: - if isinstance(variable, dict): - return {"list": [(k, variable.get(k)) for k in sorted(variable)]} - else: - return {"list": [(k, variable.getlist(k)) for k in sorted(variable)]} + return sorted(variable) except TypeError: - return {"raw": variable} + return list(variable) + + +def _process_query_dict(query_dict, keys): + """Process a QueryDict into a list of (key, sanitized_value) tuples.""" + result = [] + for k in keys: + values = query_dict.getlist(k) + # Return single value if there's only one, otherwise keep as list + value = values[0] if len(values) == 1 else values + result.append((k, safe_filter.cleanse_setting(k, value))) + return result + + +def _process_dict(dictionary, keys): + """Process a dictionary into a list of (key, sanitized_value) tuples.""" + return [(k, safe_filter.cleanse_setting(k, dictionary.get(k))) for k in keys] def get_stack(context=1) -> list[stubs.InspectStack]: diff --git a/docs/changes.rst b/docs/changes.rst index 5be81b2cb..7f8fabbc5 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,7 @@ Pending ------- * Added hook to RedirectsPanel for subclass customization. +* Added feature to sanitize sensitive data in the Request Panel. 5.1.0 (2025-03-20) ------------------ diff --git a/tests/panels/test_request.py b/tests/panels/test_request.py index 707b50bb4..2eb7ba610 100644 --- a/tests/panels/test_request.py +++ b/tests/panels/test_request.py @@ -136,3 +136,76 @@ def test_session_list_sorted_or_not(self): self.panel.generate_stats(self.request, response) panel_stats = self.panel.get_stats() self.assertEqual(panel_stats["session"], data) + + def test_sensitive_post_data_sanitized(self): + """Test that sensitive POST data is redacted.""" + self.request.POST = {"username": "testuser", "password": "secret123"} + response = self.panel.process_request(self.request) + self.panel.generate_stats(self.request, response) + + # Check that password is redacted in panel content + content = self.panel.content + self.assertIn("username", content) + self.assertIn("testuser", content) + self.assertIn("password", content) + self.assertNotIn("secret123", content) + self.assertIn("********************", content) + + def test_sensitive_get_data_sanitized(self): + """Test that sensitive GET data is redacted.""" + self.request.GET = {"api_key": "abc123", "q": "search term"} + response = self.panel.process_request(self.request) + self.panel.generate_stats(self.request, response) + + # Check that api_key is redacted in panel content + content = self.panel.content + self.assertIn("api_key", content) + self.assertNotIn("abc123", content) + self.assertIn("********************", content) + self.assertIn("q", content) + self.assertIn("search term", content) + + def test_sensitive_cookie_data_sanitized(self): + """Test that sensitive cookie data is redacted.""" + self.request.COOKIES = {"session_id": "abc123", "auth_token": "xyz789"} + response = self.panel.process_request(self.request) + self.panel.generate_stats(self.request, response) + + # Check that auth_token is redacted in panel content + content = self.panel.content + self.assertIn("session_id", content) + self.assertIn("abc123", content) + self.assertIn("auth_token", content) + self.assertNotIn("xyz789", content) + self.assertIn("********************", content) + + def test_sensitive_session_data_sanitized(self): + """Test that sensitive session data is redacted.""" + self.request.session = {"user_id": 123, "auth_token": "xyz789"} + response = self.panel.process_request(self.request) + self.panel.generate_stats(self.request, response) + + # Check that auth_token is redacted in panel content + content = self.panel.content + self.assertIn("user_id", content) + self.assertIn("123", content) + self.assertIn("auth_token", content) + self.assertNotIn("xyz789", content) + self.assertIn("********************", content) + + def test_querydict_sanitized(self): + """Test that sensitive data in QueryDict objects is properly redacted.""" + query_dict = QueryDict("username=testuser&password=secret123&token=abc456") + self.request.GET = query_dict + response = self.panel.process_request(self.request) + self.panel.generate_stats(self.request, response) + + # Check that sensitive data is redacted in panel content + content = self.panel.content + self.assertIn("username", content) + self.assertIn("testuser", content) + self.assertIn("password", content) + self.assertNotIn("secret123", content) + self.assertIn("token", content) + self.assertNotIn("abc456", content) + self.assertIn("********************", content) diff --git a/tests/test_utils.py b/tests/test_utils.py index 26bfce005..646b6a5ad 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,6 @@ import unittest +from django.http import QueryDict from django.test import override_settings import debug_toolbar.utils @@ -8,6 +9,7 @@ get_stack, get_stack_trace, render_stacktrace, + sanitize_and_sort_request_vars, tidy_stacktrace, ) @@ -109,3 +111,63 @@ def __init__(self, value): rendered_stack_2 = render_stacktrace(stack_2_wrapper.value) self.assertNotIn("test_locals_value_1", rendered_stack_2) self.assertIn("test_locals_value_2", rendered_stack_2) + + +class SanitizeAndSortRequestVarsTestCase(unittest.TestCase): + """Tests for the sanitize_and_sort_request_vars function.""" + + def test_dict_sanitization(self): + """Test sanitization of a regular dictionary.""" + test_dict = { + "username": "testuser", + "password": "secret123", + "api_key": "abc123", + } + result = sanitize_and_sort_request_vars(test_dict) + + # Convert to dict for easier testing + result_dict = dict(result["list"]) + + self.assertEqual(result_dict["username"], "testuser") + self.assertEqual(result_dict["password"], "********************") + self.assertEqual(result_dict["api_key"], "********************") + + def test_querydict_sanitization(self): + """Test sanitization of a QueryDict.""" + query_dict = QueryDict("username=testuser&password=secret123&api_key=abc123") + result = sanitize_and_sort_request_vars(query_dict) + + # Convert to dict for easier testing + result_dict = dict(result["list"]) + + self.assertEqual(result_dict["username"], "testuser") + self.assertEqual(result_dict["password"], "********************") + self.assertEqual(result_dict["api_key"], "********************") + + def test_non_sortable_dict_keys(self): + """Test dictionary with keys that can't be sorted.""" + test_dict = { + 1: "one", + "2": "two", + None: "none", + } + result = sanitize_and_sort_request_vars(test_dict) + self.assertEqual(len(result["list"]), 3) + result_dict = dict(result["list"]) + self.assertEqual(result_dict[1], "one") + self.assertEqual(result_dict["2"], "two") + self.assertEqual(result_dict[None], "none") + + def test_querydict_multiple_values(self): + """Test QueryDict with multiple values for the same key.""" + query_dict = QueryDict("name=bar1&name=bar2&title=value") + result = sanitize_and_sort_request_vars(query_dict) + result_dict = dict(result["list"]) + self.assertEqual(result_dict["name"], ["bar1", "bar2"]) + self.assertEqual(result_dict["title"], "value") + + def test_non_dict_input(self): + """Test handling of non-dict input.""" + test_input = ["not", "a", "dict"] + result = sanitize_and_sort_request_vars(test_input) + self.assertEqual(result["raw"], test_input) From fcae84dca5cb6b22fdbc05191e09da46e263574a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 07:38:41 +0100 Subject: [PATCH 178/200] [pre-commit.ci] pre-commit autoupdate (#2112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2) - [github.com/abravalheri/validate-pyproject: v0.24 → v0.24.1](https://github.com/abravalheri/validate-pyproject/compare/v0.24...v0.24.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee54d2d5d..9d2134b82 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.0' + rev: 'v0.11.2' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -39,6 +39,6 @@ repos: hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.24 + rev: v0.24.1 hooks: - id: validate-pyproject From 7de5a30550b5871d8afdd11cf8feb78e8c6f3948 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 22:09:07 +0200 Subject: [PATCH 179/200] [pre-commit.ci] pre-commit autoupdate (#2115) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d2134b82..7978a3ba9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/adamchainz/django-upgrade - rev: 1.23.1 + rev: 1.24.0 hooks: - id: django-upgrade args: [--target-version, "4.2"] From 11e28f7e629feed807c3e5519a4b0f4195fbbc31 Mon Sep 17 00:00:00 2001 From: Prashant Andoriya <121665385+andoriyaprashant@users.noreply.github.com> Date: Tue, 1 Apr 2025 01:48:13 +0530 Subject: [PATCH 180/200] Dark Mode Conflict in Pygments Fixed (#2111) --- .../static/debug_toolbar/css/toolbar.css | 500 +++++++++++++++++- docs/changes.rst | 1 + 2 files changed, 500 insertions(+), 1 deletion(-) diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index 3d0d34e6c..a45e8a670 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -59,7 +59,7 @@ } #djDebug[data-theme="dark"] { - --djdt-font-color: #8393a7; + --djdt-font-color: #f8f8f2; --djdt-background-color: #1e293bff; --djdt-panel-content-background-color: #0f1729ff; --djdt-panel-content-table-background-color: var(--djdt-background-color); @@ -829,6 +829,504 @@ To regenerate: color: #666666; } /* Literal.Number.Integer.Long */ +@media (prefers-color-scheme: dark) { + :root { + #djDebug .highlight .hll { + background-color: #f1fa8c; + } + #djDebug .highlight { + background: #282a36; + color: #f8f8f2; + } + #djDebug .highlight .c { + color: #6272a4; + } /* Comment */ + #djDebug .highlight .err { + color: #f8f8f2; + } /* Error */ + #djDebug .highlight .g { + color: #f8f8f2; + } /* Generic */ + #djDebug .highlight .k { + color: #ff79c6; + } /* Keyword */ + #djDebug .highlight .l { + color: #f8f8f2; + } /* Literal */ + #djDebug .highlight .n { + color: #f8f8f2; + } /* Name */ + #djDebug .highlight .o { + color: #ff79c6; + } /* Operator */ + #djDebug .highlight .x { + color: #f8f8f2; + } /* Other */ + #djDebug .highlight .p { + color: #f8f8f2; + } /* Punctuation */ + #djDebug .highlight .ch { + color: #6272a4; + } /* Comment.Hashbang */ + #djDebug .highlight .cm { + color: #6272a4; + } /* Comment.Multiline */ + #djDebug .highlight .cp { + color: #ff79c6; + } /* Comment.Preproc */ + #djDebug .highlight .cpf { + color: #6272a4; + } /* Comment.PreprocFile */ + #djDebug .highlight .c1 { + color: #6272a4; + } /* Comment.Single */ + #djDebug .highlight .cs { + color: #6272a4; + } /* Comment.Special */ + #djDebug .highlight .gd { + color: #8b080b; + } /* Generic.Deleted */ + #djDebug .highlight .ge { + color: #f8f8f2; + text-decoration: underline; + } /* Generic.Emph */ + #djDebug .highlight .gr { + color: #f8f8f2; + } /* Generic.Error */ + #djDebug .highlight .gh { + color: #f8f8f2; + font-weight: bold; + } /* Generic.Heading */ + #djDebug .highlight .gi { + color: #f8f8f2; + font-weight: bold; + } /* Generic.Inserted */ + #djDebug .highlight .go { + color: #44475a; + } /* Generic.Output */ + #djDebug .highlight .gp { + color: #f8f8f2; + } /* Generic.Prompt */ + #djDebug .highlight .gs { + color: #f8f8f2; + } /* Generic.Strong */ + #djDebug .highlight .gu { + color: #f8f8f2; + font-weight: bold; + } /* Generic.Subheading */ + #djDebug .highlight .gt { + color: #f8f8f2; + } /* Generic.Traceback */ + #djDebug .highlight .kc { + color: #ff79c6; + } /* Keyword.Constant */ + #djDebug .highlight .kd { + color: #8be9fd; + font-style: italic; + } /* Keyword.Declaration */ + #djDebug .highlight .kn { + color: #ff79c6; + } /* Keyword.Namespace */ + #djDebug .highlight .kp { + color: #ff79c6; + } /* Keyword.Pseudo */ + #djDebug .highlight .kr { + color: #ff79c6; + } /* Keyword.Reserved */ + #djDebug .highlight .kt { + color: #8be9fd; + } /* Keyword.Type */ + #djDebug .highlight .ld { + color: #f8f8f2; + } /* Literal.Date */ + #djDebug .highlight .m { + color: #bd93f9; + } /* Literal.Number */ + #djDebug .highlight .s { + color: #f1fa8c; + } /* Literal.String */ + #djDebug .highlight .na { + color: #50fa7b; + } /* Name.Attribute */ + #djDebug .highlight .nb { + color: #8be9fd; + font-style: italic; + } /* Name.Builtin */ + #djDebug .highlight .nc { + color: #50fa7b; + } /* Name.Class */ + #djDebug .highlight .no { + color: #f8f8f2; + } /* Name.Constant */ + #djDebug .highlight .nd { + color: #f8f8f2; + } /* Name.Decorator */ + #djDebug .highlight .ni { + color: #f8f8f2; + } /* Name.Entity */ + #djDebug .highlight .ne { + color: #f8f8f2; + } /* Name.Exception */ + #djDebug .highlight .nf { + color: #50fa7b; + } /* Name.Function */ + #djDebug .highlight .nl { + color: #8be9fd; + font-style: italic; + } /* Name.Label */ + #djDebug .highlight .nn { + color: #f8f8f2; + } /* Name.Namespace */ + #djDebug .highlight .nx { + color: #f8f8f2; + } /* Name.Other */ + #djDebug .highlight .py { + color: #f8f8f2; + } /* Name.Property */ + #djDebug .highlight .nt { + color: #ff79c6; + } /* Name.Tag */ + #djDebug .highlight .nv { + color: #8be9fd; + font-style: italic; + } /* Name.Variable */ + #djDebug .highlight .ow { + color: #ff79c6; + } /* Operator.Word */ + #djDebug .highlight .w { + color: #f8f8f2; + } /* Text.Whitespace */ + #djDebug .highlight .mb { + color: #bd93f9; + } /* Literal.Number.Bin */ + #djDebug .highlight .mf { + color: #bd93f9; + } /* Literal.Number.Float */ + #djDebug .highlight .mh { + color: #bd93f9; + } /* Literal.Number.Hex */ + #djDebug .highlight .mi { + color: #bd93f9; + } /* Literal.Number.Integer */ + #djDebug .highlight .mo { + color: #bd93f9; + } /* Literal.Number.Oct */ + #djDebug .highlight .sa { + color: #f1fa8c; + } /* Literal.String.Affix */ + #djDebug .highlight .sb { + color: #f1fa8c; + } /* Literal.String.Backtick */ + #djDebug .highlight .sc { + color: #f1fa8c; + } /* Literal.String.Char */ + #djDebug .highlight .dl { + color: #f1fa8c; + } /* Literal.String.Delimiter */ + #djDebug .highlight .sd { + color: #f1fa8c; + } /* Literal.String.Doc */ + #djDebug .highlight .s2 { + color: #f1fa8c; + } /* Literal.String.Double */ + #djDebug .highlight .se { + color: #f1fa8c; + } /* Literal.String.Escape */ + #djDebug .highlight .sh { + color: #f1fa8c; + } /* Literal.String.Heredoc */ + #djDebug .highlight .si { + color: #f1fa8c; + } /* Literal.String.Interpol */ + #djDebug .highlight .sx { + color: #f1fa8c; + } /* Literal.String.Other */ + #djDebug .highlight .sr { + color: #f1fa8c; + } /* Literal.String.Regex */ + #djDebug .highlight .s1 { + color: #f1fa8c; + } /* Literal.String.Single */ + #djDebug .highlight .ss { + color: #f1fa8c; + } /* Literal.String.Symbol */ + #djDebug .highlight .bp { + color: #f8f8f2; + font-style: italic; + } /* Name.Builtin.Pseudo */ + #djDebug .highlight .fm { + color: #50fa7b; + } /* Name.Function.Magic */ + #djDebug .highlight .vc { + color: #8be9fd; + font-style: italic; + } /* Name.Variable.Class */ + #djDebug .highlight .vg { + color: #8be9fd; + font-style: italic; + } /* Name.Variable.Global */ + #djDebug .highlight .vi { + color: #8be9fd; + font-style: italic; + } /* Name.Variable.Instance */ + #djDebug .highlight .vm { + color: #8be9fd; + font-style: italic; + } /* Name.Variable.Magic */ + #djDebug .highlight .il { + color: #bd93f9; + } /* Literal.Number.Integer.Long */ + } +} + +#djDebug[data-theme="dark"] { + #djDebug .highlight .hll { + background-color: #f1fa8c; + } + #djDebug .highlight { + background: #282a36; + color: #f8f8f2; + } + #djDebug .highlight .c { + color: #6272a4; + } /* Comment */ + #djDebug .highlight .err { + color: #f8f8f2; + } /* Error */ + #djDebug .highlight .g { + color: #f8f8f2; + } /* Generic */ + #djDebug .highlight .k { + color: #ff79c6; + } /* Keyword */ + #djDebug .highlight .l { + color: #f8f8f2; + } /* Literal */ + #djDebug .highlight .n { + color: #f8f8f2; + } /* Name */ + #djDebug .highlight .o { + color: #ff79c6; + } /* Operator */ + #djDebug .highlight .x { + color: #f8f8f2; + } /* Other */ + #djDebug .highlight .p { + color: #f8f8f2; + } /* Punctuation */ + #djDebug .highlight .ch { + color: #6272a4; + } /* Comment.Hashbang */ + #djDebug .highlight .cm { + color: #6272a4; + } /* Comment.Multiline */ + #djDebug .highlight .cp { + color: #ff79c6; + } /* Comment.Preproc */ + #djDebug .highlight .cpf { + color: #6272a4; + } /* Comment.PreprocFile */ + #djDebug .highlight .c1 { + color: #6272a4; + } /* Comment.Single */ + #djDebug .highlight .cs { + color: #6272a4; + } /* Comment.Special */ + #djDebug .highlight .gd { + color: #8b080b; + } /* Generic.Deleted */ + #djDebug .highlight .ge { + color: #f8f8f2; + text-decoration: underline; + } /* Generic.Emph */ + #djDebug .highlight .gr { + color: #f8f8f2; + } /* Generic.Error */ + #djDebug .highlight .gh { + color: #f8f8f2; + font-weight: bold; + } /* Generic.Heading */ + #djDebug .highlight .gi { + color: #f8f8f2; + font-weight: bold; + } /* Generic.Inserted */ + #djDebug .highlight .go { + color: #44475a; + } /* Generic.Output */ + #djDebug .highlight .gp { + color: #f8f8f2; + } /* Generic.Prompt */ + #djDebug .highlight .gs { + color: #f8f8f2; + } /* Generic.Strong */ + #djDebug .highlight .gu { + color: #f8f8f2; + font-weight: bold; + } /* Generic.Subheading */ + #djDebug .highlight .gt { + color: #f8f8f2; + } /* Generic.Traceback */ + #djDebug .highlight .kc { + color: #ff79c6; + } /* Keyword.Constant */ + #djDebug .highlight .kd { + color: #8be9fd; + font-style: italic; + } /* Keyword.Declaration */ + #djDebug .highlight .kn { + color: #ff79c6; + } /* Keyword.Namespace */ + #djDebug .highlight .kp { + color: #ff79c6; + } /* Keyword.Pseudo */ + #djDebug .highlight .kr { + color: #ff79c6; + } /* Keyword.Reserved */ + #djDebug .highlight .kt { + color: #8be9fd; + } /* Keyword.Type */ + #djDebug .highlight .ld { + color: #f8f8f2; + } /* Literal.Date */ + #djDebug .highlight .m { + color: #bd93f9; + } /* Literal.Number */ + #djDebug .highlight .s { + color: #f1fa8c; + } /* Literal.String */ + #djDebug .highlight .na { + color: #50fa7b; + } /* Name.Attribute */ + #djDebug .highlight .nb { + color: #8be9fd; + font-style: italic; + } /* Name.Builtin */ + #djDebug .highlight .nc { + color: #50fa7b; + } /* Name.Class */ + #djDebug .highlight .no { + color: #f8f8f2; + } /* Name.Constant */ + #djDebug .highlight .nd { + color: #f8f8f2; + } /* Name.Decorator */ + #djDebug .highlight .ni { + color: #f8f8f2; + } /* Name.Entity */ + #djDebug .highlight .ne { + color: #f8f8f2; + } /* Name.Exception */ + #djDebug .highlight .nf { + color: #50fa7b; + } /* Name.Function */ + #djDebug .highlight .nl { + color: #8be9fd; + font-style: italic; + } /* Name.Label */ + #djDebug .highlight .nn { + color: #f8f8f2; + } /* Name.Namespace */ + #djDebug .highlight .nx { + color: #f8f8f2; + } /* Name.Other */ + #djDebug .highlight .py { + color: #f8f8f2; + } /* Name.Property */ + #djDebug .highlight .nt { + color: #ff79c6; + } /* Name.Tag */ + #djDebug .highlight .nv { + color: #8be9fd; + font-style: italic; + } /* Name.Variable */ + #djDebug .highlight .ow { + color: #ff79c6; + } /* Operator.Word */ + #djDebug .highlight .w { + color: #f8f8f2; + } /* Text.Whitespace */ + #djDebug .highlight .mb { + color: #bd93f9; + } /* Literal.Number.Bin */ + #djDebug .highlight .mf { + color: #bd93f9; + } /* Literal.Number.Float */ + #djDebug .highlight .mh { + color: #bd93f9; + } /* Literal.Number.Hex */ + #djDebug .highlight .mi { + color: #bd93f9; + } /* Literal.Number.Integer */ + #djDebug .highlight .mo { + color: #bd93f9; + } /* Literal.Number.Oct */ + #djDebug .highlight .sa { + color: #f1fa8c; + } /* Literal.String.Affix */ + #djDebug .highlight .sb { + color: #f1fa8c; + } /* Literal.String.Backtick */ + #djDebug .highlight .sc { + color: #f1fa8c; + } /* Literal.String.Char */ + #djDebug .highlight .dl { + color: #f1fa8c; + } /* Literal.String.Delimiter */ + #djDebug .highlight .sd { + color: #f1fa8c; + } /* Literal.String.Doc */ + #djDebug .highlight .s2 { + color: #f1fa8c; + } /* Literal.String.Double */ + #djDebug .highlight .se { + color: #f1fa8c; + } /* Literal.String.Escape */ + #djDebug .highlight .sh { + color: #f1fa8c; + } /* Literal.String.Heredoc */ + #djDebug .highlight .si { + color: #f1fa8c; + } /* Literal.String.Interpol */ + #djDebug .highlight .sx { + color: #f1fa8c; + } /* Literal.String.Other */ + #djDebug .highlight .sr { + color: #f1fa8c; + } /* Literal.String.Regex */ + #djDebug .highlight .s1 { + color: #f1fa8c; + } /* Literal.String.Single */ + #djDebug .highlight .ss { + color: #f1fa8c; + } /* Literal.String.Symbol */ + #djDebug .highlight .bp { + color: #f8f8f2; + font-style: italic; + } /* Name.Builtin.Pseudo */ + #djDebug .highlight .fm { + color: #50fa7b; + } /* Name.Function.Magic */ + #djDebug .highlight .vc { + color: #8be9fd; + font-style: italic; + } /* Name.Variable.Class */ + #djDebug .highlight .vg { + color: #8be9fd; + font-style: italic; + } /* Name.Variable.Global */ + #djDebug .highlight .vi { + color: #8be9fd; + font-style: italic; + } /* Name.Variable.Instance */ + #djDebug .highlight .vm { + color: #8be9fd; + font-style: italic; + } /* Name.Variable.Magic */ + #djDebug .highlight .il { + color: #bd93f9; + } /* Literal.Number.Integer.Long */ +} + #djDebug svg.djDebugLineChart { width: 100%; height: 1.5em; diff --git a/docs/changes.rst b/docs/changes.rst index 7f8fabbc5..7911e9e4d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -6,6 +6,7 @@ Pending * Added hook to RedirectsPanel for subclass customization. * Added feature to sanitize sensitive data in the Request Panel. +* Fixed dark mode conflict in code block toolbar CSS 5.1.0 (2025-03-20) ------------------ From 1ea7c95b835c70356a558d010407759109032a71 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Wed, 2 Apr 2025 13:26:35 +0200 Subject: [PATCH 181/200] Fix #2109: Recursively unwrap loaders to support template partials (#2117) --- debug_toolbar/panels/templates/views.py | 15 +++++++++------ docs/changes.rst | 3 +++ tests/panels/test_template.py | 18 ++++++++++++++++++ tests/settings.py | 5 +++++ tox.ini | 1 + 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/debug_toolbar/panels/templates/views.py b/debug_toolbar/panels/templates/views.py index 898639c54..b8a0a376f 100644 --- a/debug_toolbar/panels/templates/views.py +++ b/debug_toolbar/panels/templates/views.py @@ -27,15 +27,18 @@ def template_source(request): template_name = request.GET.get("template", template_origin_name) final_loaders = [] - loaders = Engine.get_default().template_loaders + loaders = list(Engine.get_default().template_loaders) + + while loaders: + loader = loaders.pop(0) - for loader in loaders: if loader is not None: - # When the loader has loaders associated with it, - # append those loaders to the list. This occurs with - # django.template.loaders.cached.Loader + # Recursively unwrap loaders until we get to loaders which do not + # themselves wrap other loaders. This adds support for + # django.template.loaders.cached.Loader and the + # django-template-partials loader (possibly among others) if hasattr(loader, "loaders"): - final_loaders += loader.loaders + loaders.extend(loader.loaders) else: final_loaders.append(loader) diff --git a/docs/changes.rst b/docs/changes.rst index 7911e9e4d..a4a45afc0 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -7,6 +7,9 @@ Pending * Added hook to RedirectsPanel for subclass customization. * Added feature to sanitize sensitive data in the Request Panel. * Fixed dark mode conflict in code block toolbar CSS +* Added support for using django-template-partials with the template panel's + source view functionality. The same change possibly adds support for other + template loaders. 5.1.0 (2025-03-20) ------------------ diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index 636e88a23..44ac4ff0d 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -132,6 +132,24 @@ def test_lazyobject_eval(self): self.panel.generate_stats(self.request, response) self.assertIn("lazy_value", self.panel.content) + @override_settings( + DEBUG=True, + DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"], + ) + def test_template_source(self): + from django.core import signing + from django.template.loader import get_template + + template = get_template("basic.html") + url = "/__debug__/template_source/" + data = { + "template": template.template.name, + "template_origin": signing.dumps(template.template.origin.name), + } + + response = self.client.get(url, data) + self.assertEqual(response.status_code, 200) + @override_settings( DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"] diff --git a/tests/settings.py b/tests/settings.py index 0bf88bec1..12561fb11 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -7,6 +7,7 @@ # Quick-start development settings - unsuitable for production +DEBUG = False SECRET_KEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" INTERNAL_IPS = ["127.0.0.1"] @@ -27,6 +28,10 @@ "django.contrib.messages", "django.contrib.staticfiles", "debug_toolbar", + # We are not actively using template-partials; we just want more nesting + # in our template loader configuration, see + # https://github.com/django-commons/django-debug-toolbar/issues/2109 + "template_partials", "tests", ] diff --git a/tox.ini b/tox.ini index c8f4a6815..e2dcdd6c6 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,7 @@ deps = selenium>=4.8.0 sqlparse django-csp + django-template-partials passenv= CI COVERAGE_ARGS From 66361c53224428666006438d1de68bd6fba96fdb Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Wed, 2 Apr 2025 13:30:18 +0200 Subject: [PATCH 182/200] Refs #2096: The theme selector now controls all colors (#2116) Previously, if the system preference was dark mode and the user explicitly selected the light theme, the @media block still interferred with the styling. This is fixed by only evaluating the color scheme preference when initializing the toolbar and later only looking at our own selected theme. Also, removed the DEFAULT_THEME setting; falling back to system defaults seems much better to me. --- debug_toolbar/settings.py | 1 - .../static/debug_toolbar/css/toolbar.css | 915 ++++++------------ .../static/debug_toolbar/js/toolbar.js | 39 +- .../templates/debug_toolbar/base.html | 3 +- docs/changes.rst | 5 +- docs/configuration.rst | 7 - tests/test_integration.py | 28 +- 7 files changed, 366 insertions(+), 632 deletions(-) diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index e0be35ea8..59d538a0b 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -45,7 +45,6 @@ "TOOLBAR_LANGUAGE": None, "IS_RUNNING_TESTS": "test" in sys.argv, "UPDATE_ON_FETCH": False, - "DEFAULT_THEME": "auto", } diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index a45e8a670..e47dcc975 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -35,29 +35,6 @@ --djdt-raw-border-color: var(--djdt-table-border-color); } -@media (prefers-color-scheme: dark) { - :root { - --djdt-font-color: #f8f8f2; - --djdt-background-color: #1e293bff; - --djdt-panel-content-background-color: #0f1729ff; - --djdt-panel-title-background-color: #242432; - --djdt-djdt-panel-content-table-strip-background-color: #324154ff; - --djdt--highlighted-background-color: #2c2a7dff; - --djdt-toggle-template-background-color: #282755; - - --djdt-sql-font-color: var(--djdt-font-color); - --djdt-pre-text-color: var(--djdt-font-color); - --djdt-path-and-locals: #65758cff; - --djdt-stack-span-color: #7c8fa4; - --djdt-template-highlight-color: var(--djdt-stack-span-color); - - --djdt-table-border-color: #324154ff; - --djdt-button-border-color: var(--djdt-table-border-color); - --djdt-pre-border-color: var(--djdt-table-border-color); - --djdt-raw-border-color: var(--djdt-table-border-color); - } -} - #djDebug[data-theme="dark"] { --djdt-font-color: #f8f8f2; --djdt-background-color: #1e293bff; @@ -569,763 +546,511 @@ To regenerate: from pygments.formatters import HtmlFormatter print(HtmlFormatter(wrapcode=True).get_style_defs()) */ -#djDebug .highlight pre { +#djDebug[data-theme="light"] .highlight pre { line-height: 125%; } -#djDebug .highlight td.linenos .normal { +#djDebug[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -#djDebug .highlight span.linenos { +#djDebug[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -#djDebug .highlight td.linenos .special { +#djDebug[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -#djDebug .highlight span.linenos.special { +#djDebug[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -#djDebug .highlight .hll { +#djDebug[data-theme="light"] .highlight .hll { background-color: #ffffcc; } -#djDebug .highlight .c { +#djDebug[data-theme="light"] .highlight .c { color: #3d7b7b; font-style: italic; } /* Comment */ -#djDebug .highlight .err { +#djDebug[data-theme="light"] .highlight .err { border: 1px solid #ff0000; } /* Error */ -#djDebug .highlight .k { +#djDebug[data-theme="light"] .highlight .k { color: #008000; font-weight: bold; } /* Keyword */ -#djDebug .highlight .o { +#djDebug[data-theme="light"] .highlight .o { color: #666666; } /* Operator */ -#djDebug .highlight .ch { +#djDebug[data-theme="light"] .highlight .ch { color: #3d7b7b; font-style: italic; } /* Comment.Hashbang */ -#djDebug .highlight .cm { +#djDebug[data-theme="light"] .highlight .cm { color: #3d7b7b; font-style: italic; } /* Comment.Multiline */ -#djDebug .highlight .cp { +#djDebug[data-theme="light"] .highlight .cp { color: #9c6500; } /* Comment.Preproc */ -#djDebug .highlight .cpf { +#djDebug[data-theme="light"] .highlight .cpf { color: #3d7b7b; font-style: italic; } /* Comment.PreprocFile */ -#djDebug .highlight .c1 { +#djDebug[data-theme="light"] .highlight .c1 { color: #3d7b7b; font-style: italic; } /* Comment.Single */ -#djDebug .highlight .cs { +#djDebug[data-theme="light"] .highlight .cs { color: #3d7b7b; font-style: italic; } /* Comment.Special */ -#djDebug .highlight .gd { +#djDebug[data-theme="light"] .highlight .gd { color: #a00000; } /* Generic.Deleted */ -#djDebug .highlight .ge { +#djDebug[data-theme="light"] .highlight .ge { font-style: italic; } /* Generic.Emph */ -#djDebug .highlight .ges { +#djDebug[data-theme="light"] .highlight .ges { font-weight: bold; font-style: italic; } /* Generic.EmphStrong */ -#djDebug .highlight .gr { +#djDebug[data-theme="light"] .highlight .gr { color: #e40000; } /* Generic.Error */ -#djDebug .highlight .gh { +#djDebug[data-theme="light"] .highlight .gh { color: #000080; font-weight: bold; } /* Generic.Heading */ -#djDebug .highlight .gi { +#djDebug[data-theme="light"] .highlight .gi { color: #008400; } /* Generic.Inserted */ -#djDebug .highlight .go { +#djDebug[data-theme="light"] .highlight .go { color: #717171; } /* Generic.Output */ -#djDebug .highlight .gp { +#djDebug[data-theme="light"] .highlight .gp { color: #000080; font-weight: bold; } /* Generic.Prompt */ -#djDebug .highlight .gs { +#djDebug[data-theme="light"] .highlight .gs { font-weight: bold; } /* Generic.Strong */ -#djDebug .highlight .gu { +#djDebug[data-theme="light"] .highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */ -#djDebug .highlight .gt { +#djDebug[data-theme="light"] .highlight .gt { color: #0044dd; } /* Generic.Traceback */ -#djDebug .highlight .kc { +#djDebug[data-theme="light"] .highlight .kc { color: #008000; font-weight: bold; } /* Keyword.Constant */ -#djDebug .highlight .kd { +#djDebug[data-theme="light"] .highlight .kd { color: #008000; font-weight: bold; } /* Keyword.Declaration */ -#djDebug .highlight .kn { +#djDebug[data-theme="light"] .highlight .kn { color: #008000; font-weight: bold; } /* Keyword.Namespace */ -#djDebug .highlight .kp { +#djDebug[data-theme="light"] .highlight .kp { color: #008000; } /* Keyword.Pseudo */ -#djDebug .highlight .kr { +#djDebug[data-theme="light"] .highlight .kr { color: #008000; font-weight: bold; } /* Keyword.Reserved */ -#djDebug .highlight .kt { +#djDebug[data-theme="light"] .highlight .kt { color: #b00040; } /* Keyword.Type */ -#djDebug .highlight .m { +#djDebug[data-theme="light"] .highlight .m { color: #666666; } /* Literal.Number */ -#djDebug .highlight .s { +#djDebug[data-theme="light"] .highlight .s { color: #ba2121; } /* Literal.String */ -#djDebug .highlight .na { +#djDebug[data-theme="light"] .highlight .na { color: #687822; } /* Name.Attribute */ -#djDebug .highlight .nb { +#djDebug[data-theme="light"] .highlight .nb { color: #008000; } /* Name.Builtin */ -#djDebug .highlight .nc { +#djDebug[data-theme="light"] .highlight .nc { color: #0000ff; font-weight: bold; } /* Name.Class */ -#djDebug .highlight .no { +#djDebug[data-theme="light"] .highlight .no { color: #880000; } /* Name.Constant */ -#djDebug .highlight .nd { +#djDebug[data-theme="light"] .highlight .nd { color: #aa22ff; } /* Name.Decorator */ -#djDebug .highlight .ni { +#djDebug[data-theme="light"] .highlight .ni { color: #717171; font-weight: bold; } /* Name.Entity */ -#djDebug .highlight .ne { +#djDebug[data-theme="light"] .highlight .ne { color: #cb3f38; font-weight: bold; } /* Name.Exception */ -#djDebug .highlight .nf { +#djDebug[data-theme="light"] .highlight .nf { color: #0000ff; } /* Name.Function */ -#djDebug .highlight .nl { +#djDebug[data-theme="light"] .highlight .nl { color: #767600; } /* Name.Label */ -#djDebug .highlight .nn { +#djDebug[data-theme="light"] .highlight .nn { color: #0000ff; font-weight: bold; } /* Name.Namespace */ -#djDebug .highlight .nt { +#djDebug[data-theme="light"] .highlight .nt { color: #008000; font-weight: bold; } /* Name.Tag */ -#djDebug .highlight .nv { +#djDebug[data-theme="light"] .highlight .nv { color: #19177c; } /* Name.Variable */ -#djDebug .highlight .ow { +#djDebug[data-theme="light"] .highlight .ow { color: #aa22ff; font-weight: bold; } /* Operator.Word */ -#djDebug .highlight .w { +#djDebug[data-theme="light"] .highlight .w { color: #bbbbbb; white-space: pre-wrap; } /* Text.Whitespace */ -#djDebug .highlight .mb { +#djDebug[data-theme="light"] .highlight .mb { color: #666666; } /* Literal.Number.Bin */ -#djDebug .highlight .mf { +#djDebug[data-theme="light"] .highlight .mf { color: #666666; } /* Literal.Number.Float */ -#djDebug .highlight .mh { +#djDebug[data-theme="light"] .highlight .mh { color: #666666; } /* Literal.Number.Hex */ -#djDebug .highlight .mi { +#djDebug[data-theme="light"] .highlight .mi { color: #666666; } /* Literal.Number.Integer */ -#djDebug .highlight .mo { +#djDebug[data-theme="light"] .highlight .mo { color: #666666; } /* Literal.Number.Oct */ -#djDebug .highlight .sa { +#djDebug[data-theme="light"] .highlight .sa { color: #ba2121; } /* Literal.String.Affix */ -#djDebug .highlight .sb { +#djDebug[data-theme="light"] .highlight .sb { color: #ba2121; } /* Literal.String.Backtick */ -#djDebug .highlight .sc { +#djDebug[data-theme="light"] .highlight .sc { color: #ba2121; } /* Literal.String.Char */ -#djDebug .highlight .dl { +#djDebug[data-theme="light"] .highlight .dl { color: #ba2121; } /* Literal.String.Delimiter */ -#djDebug .highlight .sd { +#djDebug[data-theme="light"] .highlight .sd { color: #ba2121; font-style: italic; } /* Literal.String.Doc */ -#djDebug .highlight .s2 { +#djDebug[data-theme="light"] .highlight .s2 { color: #ba2121; } /* Literal.String.Double */ -#djDebug .highlight .se { +#djDebug[data-theme="light"] .highlight .se { color: #aa5d1f; font-weight: bold; } /* Literal.String.Escape */ -#djDebug .highlight .sh { +#djDebug[data-theme="light"] .highlight .sh { color: #ba2121; } /* Literal.String.Heredoc */ -#djDebug .highlight .si { +#djDebug[data-theme="light"] .highlight .si { color: #a45a77; font-weight: bold; } /* Literal.String.Interpol */ -#djDebug .highlight .sx { +#djDebug[data-theme="light"] .highlight .sx { color: #008000; } /* Literal.String.Other */ -#djDebug .highlight .sr { +#djDebug[data-theme="light"] .highlight .sr { color: #a45a77; } /* Literal.String.Regex */ -#djDebug .highlight .s1 { +#djDebug[data-theme="light"] .highlight .s1 { color: #ba2121; } /* Literal.String.Single */ -#djDebug .highlight .ss { +#djDebug[data-theme="light"] .highlight .ss { color: #19177c; } /* Literal.String.Symbol */ -#djDebug .highlight .bp { +#djDebug[data-theme="light"] .highlight .bp { color: #008000; } /* Name.Builtin.Pseudo */ -#djDebug .highlight .fm { +#djDebug[data-theme="light"] .highlight .fm { color: #0000ff; } /* Name.Function.Magic */ -#djDebug .highlight .vc { +#djDebug[data-theme="light"] .highlight .vc { color: #19177c; } /* Name.Variable.Class */ -#djDebug .highlight .vg { +#djDebug[data-theme="light"] .highlight .vg { color: #19177c; } /* Name.Variable.Global */ -#djDebug .highlight .vi { +#djDebug[data-theme="light"] .highlight .vi { color: #19177c; } /* Name.Variable.Instance */ -#djDebug .highlight .vm { +#djDebug[data-theme="light"] .highlight .vm { color: #19177c; } /* Name.Variable.Magic */ -#djDebug .highlight .il { +#djDebug[data-theme="light"] .highlight .il { color: #666666; } /* Literal.Number.Integer.Long */ -@media (prefers-color-scheme: dark) { - :root { - #djDebug .highlight .hll { - background-color: #f1fa8c; - } - #djDebug .highlight { - background: #282a36; - color: #f8f8f2; - } - #djDebug .highlight .c { - color: #6272a4; - } /* Comment */ - #djDebug .highlight .err { - color: #f8f8f2; - } /* Error */ - #djDebug .highlight .g { - color: #f8f8f2; - } /* Generic */ - #djDebug .highlight .k { - color: #ff79c6; - } /* Keyword */ - #djDebug .highlight .l { - color: #f8f8f2; - } /* Literal */ - #djDebug .highlight .n { - color: #f8f8f2; - } /* Name */ - #djDebug .highlight .o { - color: #ff79c6; - } /* Operator */ - #djDebug .highlight .x { - color: #f8f8f2; - } /* Other */ - #djDebug .highlight .p { - color: #f8f8f2; - } /* Punctuation */ - #djDebug .highlight .ch { - color: #6272a4; - } /* Comment.Hashbang */ - #djDebug .highlight .cm { - color: #6272a4; - } /* Comment.Multiline */ - #djDebug .highlight .cp { - color: #ff79c6; - } /* Comment.Preproc */ - #djDebug .highlight .cpf { - color: #6272a4; - } /* Comment.PreprocFile */ - #djDebug .highlight .c1 { - color: #6272a4; - } /* Comment.Single */ - #djDebug .highlight .cs { - color: #6272a4; - } /* Comment.Special */ - #djDebug .highlight .gd { - color: #8b080b; - } /* Generic.Deleted */ - #djDebug .highlight .ge { - color: #f8f8f2; - text-decoration: underline; - } /* Generic.Emph */ - #djDebug .highlight .gr { - color: #f8f8f2; - } /* Generic.Error */ - #djDebug .highlight .gh { - color: #f8f8f2; - font-weight: bold; - } /* Generic.Heading */ - #djDebug .highlight .gi { - color: #f8f8f2; - font-weight: bold; - } /* Generic.Inserted */ - #djDebug .highlight .go { - color: #44475a; - } /* Generic.Output */ - #djDebug .highlight .gp { - color: #f8f8f2; - } /* Generic.Prompt */ - #djDebug .highlight .gs { - color: #f8f8f2; - } /* Generic.Strong */ - #djDebug .highlight .gu { - color: #f8f8f2; - font-weight: bold; - } /* Generic.Subheading */ - #djDebug .highlight .gt { - color: #f8f8f2; - } /* Generic.Traceback */ - #djDebug .highlight .kc { - color: #ff79c6; - } /* Keyword.Constant */ - #djDebug .highlight .kd { - color: #8be9fd; - font-style: italic; - } /* Keyword.Declaration */ - #djDebug .highlight .kn { - color: #ff79c6; - } /* Keyword.Namespace */ - #djDebug .highlight .kp { - color: #ff79c6; - } /* Keyword.Pseudo */ - #djDebug .highlight .kr { - color: #ff79c6; - } /* Keyword.Reserved */ - #djDebug .highlight .kt { - color: #8be9fd; - } /* Keyword.Type */ - #djDebug .highlight .ld { - color: #f8f8f2; - } /* Literal.Date */ - #djDebug .highlight .m { - color: #bd93f9; - } /* Literal.Number */ - #djDebug .highlight .s { - color: #f1fa8c; - } /* Literal.String */ - #djDebug .highlight .na { - color: #50fa7b; - } /* Name.Attribute */ - #djDebug .highlight .nb { - color: #8be9fd; - font-style: italic; - } /* Name.Builtin */ - #djDebug .highlight .nc { - color: #50fa7b; - } /* Name.Class */ - #djDebug .highlight .no { - color: #f8f8f2; - } /* Name.Constant */ - #djDebug .highlight .nd { - color: #f8f8f2; - } /* Name.Decorator */ - #djDebug .highlight .ni { - color: #f8f8f2; - } /* Name.Entity */ - #djDebug .highlight .ne { - color: #f8f8f2; - } /* Name.Exception */ - #djDebug .highlight .nf { - color: #50fa7b; - } /* Name.Function */ - #djDebug .highlight .nl { - color: #8be9fd; - font-style: italic; - } /* Name.Label */ - #djDebug .highlight .nn { - color: #f8f8f2; - } /* Name.Namespace */ - #djDebug .highlight .nx { - color: #f8f8f2; - } /* Name.Other */ - #djDebug .highlight .py { - color: #f8f8f2; - } /* Name.Property */ - #djDebug .highlight .nt { - color: #ff79c6; - } /* Name.Tag */ - #djDebug .highlight .nv { - color: #8be9fd; - font-style: italic; - } /* Name.Variable */ - #djDebug .highlight .ow { - color: #ff79c6; - } /* Operator.Word */ - #djDebug .highlight .w { - color: #f8f8f2; - } /* Text.Whitespace */ - #djDebug .highlight .mb { - color: #bd93f9; - } /* Literal.Number.Bin */ - #djDebug .highlight .mf { - color: #bd93f9; - } /* Literal.Number.Float */ - #djDebug .highlight .mh { - color: #bd93f9; - } /* Literal.Number.Hex */ - #djDebug .highlight .mi { - color: #bd93f9; - } /* Literal.Number.Integer */ - #djDebug .highlight .mo { - color: #bd93f9; - } /* Literal.Number.Oct */ - #djDebug .highlight .sa { - color: #f1fa8c; - } /* Literal.String.Affix */ - #djDebug .highlight .sb { - color: #f1fa8c; - } /* Literal.String.Backtick */ - #djDebug .highlight .sc { - color: #f1fa8c; - } /* Literal.String.Char */ - #djDebug .highlight .dl { - color: #f1fa8c; - } /* Literal.String.Delimiter */ - #djDebug .highlight .sd { - color: #f1fa8c; - } /* Literal.String.Doc */ - #djDebug .highlight .s2 { - color: #f1fa8c; - } /* Literal.String.Double */ - #djDebug .highlight .se { - color: #f1fa8c; - } /* Literal.String.Escape */ - #djDebug .highlight .sh { - color: #f1fa8c; - } /* Literal.String.Heredoc */ - #djDebug .highlight .si { - color: #f1fa8c; - } /* Literal.String.Interpol */ - #djDebug .highlight .sx { - color: #f1fa8c; - } /* Literal.String.Other */ - #djDebug .highlight .sr { - color: #f1fa8c; - } /* Literal.String.Regex */ - #djDebug .highlight .s1 { - color: #f1fa8c; - } /* Literal.String.Single */ - #djDebug .highlight .ss { - color: #f1fa8c; - } /* Literal.String.Symbol */ - #djDebug .highlight .bp { - color: #f8f8f2; - font-style: italic; - } /* Name.Builtin.Pseudo */ - #djDebug .highlight .fm { - color: #50fa7b; - } /* Name.Function.Magic */ - #djDebug .highlight .vc { - color: #8be9fd; - font-style: italic; - } /* Name.Variable.Class */ - #djDebug .highlight .vg { - color: #8be9fd; - font-style: italic; - } /* Name.Variable.Global */ - #djDebug .highlight .vi { - color: #8be9fd; - font-style: italic; - } /* Name.Variable.Instance */ - #djDebug .highlight .vm { - color: #8be9fd; - font-style: italic; - } /* Name.Variable.Magic */ - #djDebug .highlight .il { - color: #bd93f9; - } /* Literal.Number.Integer.Long */ - } +#djDebug[data-theme="dark"] .highlight .hll { + background-color: #f1fa8c; } - -#djDebug[data-theme="dark"] { - #djDebug .highlight .hll { - background-color: #f1fa8c; - } - #djDebug .highlight { - background: #282a36; - color: #f8f8f2; - } - #djDebug .highlight .c { - color: #6272a4; - } /* Comment */ - #djDebug .highlight .err { - color: #f8f8f2; - } /* Error */ - #djDebug .highlight .g { - color: #f8f8f2; - } /* Generic */ - #djDebug .highlight .k { - color: #ff79c6; - } /* Keyword */ - #djDebug .highlight .l { - color: #f8f8f2; - } /* Literal */ - #djDebug .highlight .n { - color: #f8f8f2; - } /* Name */ - #djDebug .highlight .o { - color: #ff79c6; - } /* Operator */ - #djDebug .highlight .x { - color: #f8f8f2; - } /* Other */ - #djDebug .highlight .p { - color: #f8f8f2; - } /* Punctuation */ - #djDebug .highlight .ch { - color: #6272a4; - } /* Comment.Hashbang */ - #djDebug .highlight .cm { - color: #6272a4; - } /* Comment.Multiline */ - #djDebug .highlight .cp { - color: #ff79c6; - } /* Comment.Preproc */ - #djDebug .highlight .cpf { - color: #6272a4; - } /* Comment.PreprocFile */ - #djDebug .highlight .c1 { - color: #6272a4; - } /* Comment.Single */ - #djDebug .highlight .cs { - color: #6272a4; - } /* Comment.Special */ - #djDebug .highlight .gd { - color: #8b080b; - } /* Generic.Deleted */ - #djDebug .highlight .ge { - color: #f8f8f2; - text-decoration: underline; - } /* Generic.Emph */ - #djDebug .highlight .gr { - color: #f8f8f2; - } /* Generic.Error */ - #djDebug .highlight .gh { - color: #f8f8f2; - font-weight: bold; - } /* Generic.Heading */ - #djDebug .highlight .gi { - color: #f8f8f2; - font-weight: bold; - } /* Generic.Inserted */ - #djDebug .highlight .go { - color: #44475a; - } /* Generic.Output */ - #djDebug .highlight .gp { - color: #f8f8f2; - } /* Generic.Prompt */ - #djDebug .highlight .gs { - color: #f8f8f2; - } /* Generic.Strong */ - #djDebug .highlight .gu { - color: #f8f8f2; - font-weight: bold; - } /* Generic.Subheading */ - #djDebug .highlight .gt { - color: #f8f8f2; - } /* Generic.Traceback */ - #djDebug .highlight .kc { - color: #ff79c6; - } /* Keyword.Constant */ - #djDebug .highlight .kd { - color: #8be9fd; - font-style: italic; - } /* Keyword.Declaration */ - #djDebug .highlight .kn { - color: #ff79c6; - } /* Keyword.Namespace */ - #djDebug .highlight .kp { - color: #ff79c6; - } /* Keyword.Pseudo */ - #djDebug .highlight .kr { - color: #ff79c6; - } /* Keyword.Reserved */ - #djDebug .highlight .kt { - color: #8be9fd; - } /* Keyword.Type */ - #djDebug .highlight .ld { - color: #f8f8f2; - } /* Literal.Date */ - #djDebug .highlight .m { - color: #bd93f9; - } /* Literal.Number */ - #djDebug .highlight .s { - color: #f1fa8c; - } /* Literal.String */ - #djDebug .highlight .na { - color: #50fa7b; - } /* Name.Attribute */ - #djDebug .highlight .nb { - color: #8be9fd; - font-style: italic; - } /* Name.Builtin */ - #djDebug .highlight .nc { - color: #50fa7b; - } /* Name.Class */ - #djDebug .highlight .no { - color: #f8f8f2; - } /* Name.Constant */ - #djDebug .highlight .nd { - color: #f8f8f2; - } /* Name.Decorator */ - #djDebug .highlight .ni { - color: #f8f8f2; - } /* Name.Entity */ - #djDebug .highlight .ne { - color: #f8f8f2; - } /* Name.Exception */ - #djDebug .highlight .nf { - color: #50fa7b; - } /* Name.Function */ - #djDebug .highlight .nl { - color: #8be9fd; - font-style: italic; - } /* Name.Label */ - #djDebug .highlight .nn { - color: #f8f8f2; - } /* Name.Namespace */ - #djDebug .highlight .nx { - color: #f8f8f2; - } /* Name.Other */ - #djDebug .highlight .py { - color: #f8f8f2; - } /* Name.Property */ - #djDebug .highlight .nt { - color: #ff79c6; - } /* Name.Tag */ - #djDebug .highlight .nv { - color: #8be9fd; - font-style: italic; - } /* Name.Variable */ - #djDebug .highlight .ow { - color: #ff79c6; - } /* Operator.Word */ - #djDebug .highlight .w { - color: #f8f8f2; - } /* Text.Whitespace */ - #djDebug .highlight .mb { - color: #bd93f9; - } /* Literal.Number.Bin */ - #djDebug .highlight .mf { - color: #bd93f9; - } /* Literal.Number.Float */ - #djDebug .highlight .mh { - color: #bd93f9; - } /* Literal.Number.Hex */ - #djDebug .highlight .mi { - color: #bd93f9; - } /* Literal.Number.Integer */ - #djDebug .highlight .mo { - color: #bd93f9; - } /* Literal.Number.Oct */ - #djDebug .highlight .sa { - color: #f1fa8c; - } /* Literal.String.Affix */ - #djDebug .highlight .sb { - color: #f1fa8c; - } /* Literal.String.Backtick */ - #djDebug .highlight .sc { - color: #f1fa8c; - } /* Literal.String.Char */ - #djDebug .highlight .dl { - color: #f1fa8c; - } /* Literal.String.Delimiter */ - #djDebug .highlight .sd { - color: #f1fa8c; - } /* Literal.String.Doc */ - #djDebug .highlight .s2 { - color: #f1fa8c; - } /* Literal.String.Double */ - #djDebug .highlight .se { - color: #f1fa8c; - } /* Literal.String.Escape */ - #djDebug .highlight .sh { - color: #f1fa8c; - } /* Literal.String.Heredoc */ - #djDebug .highlight .si { - color: #f1fa8c; - } /* Literal.String.Interpol */ - #djDebug .highlight .sx { - color: #f1fa8c; - } /* Literal.String.Other */ - #djDebug .highlight .sr { - color: #f1fa8c; - } /* Literal.String.Regex */ - #djDebug .highlight .s1 { - color: #f1fa8c; - } /* Literal.String.Single */ - #djDebug .highlight .ss { - color: #f1fa8c; - } /* Literal.String.Symbol */ - #djDebug .highlight .bp { - color: #f8f8f2; - font-style: italic; - } /* Name.Builtin.Pseudo */ - #djDebug .highlight .fm { - color: #50fa7b; - } /* Name.Function.Magic */ - #djDebug .highlight .vc { - color: #8be9fd; - font-style: italic; - } /* Name.Variable.Class */ - #djDebug .highlight .vg { - color: #8be9fd; - font-style: italic; - } /* Name.Variable.Global */ - #djDebug .highlight .vi { - color: #8be9fd; - font-style: italic; - } /* Name.Variable.Instance */ - #djDebug .highlight .vm { - color: #8be9fd; - font-style: italic; - } /* Name.Variable.Magic */ - #djDebug .highlight .il { - color: #bd93f9; - } /* Literal.Number.Integer.Long */ +#djDebug[data-theme="dark"] .highlight { + background: #282a36; + color: #f8f8f2; } +#djDebug[data-theme="dark"] .highlight .c { + color: #6272a4; +} /* Comment */ +#djDebug[data-theme="dark"] .highlight .err { + color: #f8f8f2; +} /* Error */ +#djDebug[data-theme="dark"] .highlight .g { + color: #f8f8f2; +} /* Generic */ +#djDebug[data-theme="dark"] .highlight .k { + color: #ff79c6; +} /* Keyword */ +#djDebug[data-theme="dark"] .highlight .l { + color: #f8f8f2; +} /* Literal */ +#djDebug[data-theme="dark"] .highlight .n { + color: #f8f8f2; +} /* Name */ +#djDebug[data-theme="dark"] .highlight .o { + color: #ff79c6; +} /* Operator */ +#djDebug[data-theme="dark"] .highlight .x { + color: #f8f8f2; +} /* Other */ +#djDebug[data-theme="dark"] .highlight .p { + color: #f8f8f2; +} /* Punctuation */ +#djDebug[data-theme="dark"] .highlight .ch { + color: #6272a4; +} /* Comment.Hashbang */ +#djDebug[data-theme="dark"] .highlight .cm { + color: #6272a4; +} /* Comment.Multiline */ +#djDebug[data-theme="dark"] .highlight .cp { + color: #ff79c6; +} /* Comment.Preproc */ +#djDebug[data-theme="dark"] .highlight .cpf { + color: #6272a4; +} /* Comment.PreprocFile */ +#djDebug[data-theme="dark"] .highlight .c1 { + color: #6272a4; +} /* Comment.Single */ +#djDebug[data-theme="dark"] .highlight .cs { + color: #6272a4; +} /* Comment.Special */ +#djDebug[data-theme="dark"] .highlight .gd { + color: #8b080b; +} /* Generic.Deleted */ +#djDebug[data-theme="dark"] .highlight .ge { + color: #f8f8f2; + text-decoration: underline; +} /* Generic.Emph */ +#djDebug[data-theme="dark"] .highlight .gr { + color: #f8f8f2; +} /* Generic.Error */ +#djDebug[data-theme="dark"] .highlight .gh { + color: #f8f8f2; + font-weight: bold; +} /* Generic.Heading */ +#djDebug[data-theme="dark"] .highlight .gi { + color: #f8f8f2; + font-weight: bold; +} /* Generic.Inserted */ +#djDebug[data-theme="dark"] .highlight .go { + color: #44475a; +} /* Generic.Output */ +#djDebug[data-theme="dark"] .highlight .gp { + color: #f8f8f2; +} /* Generic.Prompt */ +#djDebug[data-theme="dark"] .highlight .gs { + color: #f8f8f2; +} /* Generic.Strong */ +#djDebug[data-theme="dark"] .highlight .gu { + color: #f8f8f2; + font-weight: bold; +} /* Generic.Subheading */ +#djDebug[data-theme="dark"] .highlight .gt { + color: #f8f8f2; +} /* Generic.Traceback */ +#djDebug[data-theme="dark"] .highlight .kc { + color: #ff79c6; +} /* Keyword.Constant */ +#djDebug[data-theme="dark"] .highlight .kd { + color: #8be9fd; + font-style: italic; +} /* Keyword.Declaration */ +#djDebug[data-theme="dark"] .highlight .kn { + color: #ff79c6; +} /* Keyword.Namespace */ +#djDebug[data-theme="dark"] .highlight .kp { + color: #ff79c6; +} /* Keyword.Pseudo */ +#djDebug[data-theme="dark"] .highlight .kr { + color: #ff79c6; +} /* Keyword.Reserved */ +#djDebug[data-theme="dark"] .highlight .kt { + color: #8be9fd; +} /* Keyword.Type */ +#djDebug[data-theme="dark"] .highlight .ld { + color: #f8f8f2; +} /* Literal.Date */ +#djDebug[data-theme="dark"] .highlight .m { + color: #bd93f9; +} /* Literal.Number */ +#djDebug[data-theme="dark"] .highlight .s { + color: #f1fa8c; +} /* Literal.String */ +#djDebug[data-theme="dark"] .highlight .na { + color: #50fa7b; +} /* Name.Attribute */ +#djDebug[data-theme="dark"] .highlight .nb { + color: #8be9fd; + font-style: italic; +} /* Name.Builtin */ +#djDebug[data-theme="dark"] .highlight .nc { + color: #50fa7b; +} /* Name.Class */ +#djDebug[data-theme="dark"] .highlight .no { + color: #f8f8f2; +} /* Name.Constant */ +#djDebug[data-theme="dark"] .highlight .nd { + color: #f8f8f2; +} /* Name.Decorator */ +#djDebug[data-theme="dark"] .highlight .ni { + color: #f8f8f2; +} /* Name.Entity */ +#djDebug[data-theme="dark"] .highlight .ne { + color: #f8f8f2; +} /* Name.Exception */ +#djDebug[data-theme="dark"] .highlight .nf { + color: #50fa7b; +} /* Name.Function */ +#djDebug[data-theme="dark"] .highlight .nl { + color: #8be9fd; + font-style: italic; +} /* Name.Label */ +#djDebug[data-theme="dark"] .highlight .nn { + color: #f8f8f2; +} /* Name.Namespace */ +#djDebug[data-theme="dark"] .highlight .nx { + color: #f8f8f2; +} /* Name.Other */ +#djDebug[data-theme="dark"] .highlight .py { + color: #f8f8f2; +} /* Name.Property */ +#djDebug[data-theme="dark"] .highlight .nt { + color: #ff79c6; +} /* Name.Tag */ +#djDebug[data-theme="dark"] .highlight .nv { + color: #8be9fd; + font-style: italic; +} /* Name.Variable */ +#djDebug[data-theme="dark"] .highlight .ow { + color: #ff79c6; +} /* Operator.Word */ +#djDebug[data-theme="dark"] .highlight .w { + color: #f8f8f2; +} /* Text.Whitespace */ +#djDebug[data-theme="dark"] .highlight .mb { + color: #bd93f9; +} /* Literal.Number.Bin */ +#djDebug[data-theme="dark"] .highlight .mf { + color: #bd93f9; +} /* Literal.Number.Float */ +#djDebug[data-theme="dark"] .highlight .mh { + color: #bd93f9; +} /* Literal.Number.Hex */ +#djDebug[data-theme="dark"] .highlight .mi { + color: #bd93f9; +} /* Literal.Number.Integer */ +#djDebug[data-theme="dark"] .highlight .mo { + color: #bd93f9; +} /* Literal.Number.Oct */ +#djDebug[data-theme="dark"] .highlight .sa { + color: #f1fa8c; +} /* Literal.String.Affix */ +#djDebug[data-theme="dark"] .highlight .sb { + color: #f1fa8c; +} /* Literal.String.Backtick */ +#djDebug[data-theme="dark"] .highlight .sc { + color: #f1fa8c; +} /* Literal.String.Char */ +#djDebug[data-theme="dark"] .highlight .dl { + color: #f1fa8c; +} /* Literal.String.Delimiter */ +#djDebug[data-theme="dark"] .highlight .sd { + color: #f1fa8c; +} /* Literal.String.Doc */ +#djDebug[data-theme="dark"] .highlight .s2 { + color: #f1fa8c; +} /* Literal.String.Double */ +#djDebug[data-theme="dark"] .highlight .se { + color: #f1fa8c; +} /* Literal.String.Escape */ +#djDebug[data-theme="dark"] .highlight .sh { + color: #f1fa8c; +} /* Literal.String.Heredoc */ +#djDebug[data-theme="dark"] .highlight .si { + color: #f1fa8c; +} /* Literal.String.Interpol */ +#djDebug[data-theme="dark"] .highlight .sx { + color: #f1fa8c; +} /* Literal.String.Other */ +#djDebug[data-theme="dark"] .highlight .sr { + color: #f1fa8c; +} /* Literal.String.Regex */ +#djDebug[data-theme="dark"] .highlight .s1 { + color: #f1fa8c; +} /* Literal.String.Single */ +#djDebug[data-theme="dark"] .highlight .ss { + color: #f1fa8c; +} /* Literal.String.Symbol */ +#djDebug[data-theme="dark"] .highlight .bp { + color: #f8f8f2; + font-style: italic; +} /* Name.Builtin.Pseudo */ +#djDebug[data-theme="dark"] .highlight .fm { + color: #50fa7b; +} /* Name.Function.Magic */ +#djDebug[data-theme="dark"] .highlight .vc { + color: #8be9fd; + font-style: italic; +} /* Name.Variable.Class */ +#djDebug[data-theme="dark"] .highlight .vg { + color: #8be9fd; + font-style: italic; +} /* Name.Variable.Global */ +#djDebug[data-theme="dark"] .highlight .vi { + color: #8be9fd; + font-style: italic; +} /* Name.Variable.Instance */ +#djDebug[data-theme="dark"] .highlight .vm { + color: #8be9fd; + font-style: italic; +} /* Name.Variable.Magic */ +#djDebug[data-theme="dark"] .highlight .il { + color: #bd93f9; +} /* Literal.Number.Integer.Long */ #djDebug svg.djDebugLineChart { width: 100%; @@ -1445,9 +1170,9 @@ To regenerate: #djToggleThemeButton > svg { margin-left: auto; } -#djDebug[data-theme="light"] #djToggleThemeButton svg.theme-light, -#djDebug[data-theme="dark"] #djToggleThemeButton svg.theme-dark, -#djDebug[data-theme="auto"] #djToggleThemeButton svg.theme-auto { +#djDebug[data-user-theme="light"] #djToggleThemeButton svg.theme-light, +#djDebug[data-user-theme="dark"] #djToggleThemeButton svg.theme-dark, +#djDebug[data-user-theme="auto"] #djToggleThemeButton svg.theme-auto { display: block; height: 1rem; width: 1rem; diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 077bc930a..19658f76e 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -212,27 +212,30 @@ const djdt = { djdt.updateOnAjax(); } + const prefersDark = window.matchMedia( + "(prefers-color-scheme: dark)" + ).matches; + const themeList = prefersDark + ? ["auto", "light", "dark"] + : ["auto", "dark", "light"]; + const setTheme = (theme) => { + djDebug.setAttribute( + "data-theme", + theme === "auto" ? (prefersDark ? "dark" : "light") : theme + ); + djDebug.setAttribute("data-user-theme", theme); + }; + // Updates the theme using user settings - const userTheme = localStorage.getItem("djdt.user-theme"); - if (userTheme !== null) { - djDebug.setAttribute("data-theme", userTheme); - } + let userTheme = localStorage.getItem("djdt.user-theme") || "auto"; + setTheme(userTheme); + // Adds the listener to the Theme Toggle Button $$.on(djDebug, "click", "#djToggleThemeButton", () => { - switch (djDebug.getAttribute("data-theme")) { - case "auto": - djDebug.setAttribute("data-theme", "light"); - localStorage.setItem("djdt.user-theme", "light"); - break; - case "light": - djDebug.setAttribute("data-theme", "dark"); - localStorage.setItem("djdt.user-theme", "dark"); - break; - default: /* dark is the default */ - djDebug.setAttribute("data-theme", "auto"); - localStorage.setItem("djdt.user-theme", "auto"); - break; - } + const index = themeList.indexOf(userTheme); + userTheme = themeList[(index + 1) % themeList.length]; + localStorage.setItem("djdt.user-theme", userTheme); + setTheme(userTheme); }); }, hidePanels() { diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index a9983250d..b5c225ac8 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -16,8 +16,7 @@ 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 }} data-update-on-fetch="{{ toolbar.config.UPDATE_ON_FETCH }}" - data-theme="{{ toolbar.config.DEFAULT_THEME }}"> + {{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }} data-update-on-fetch="{{ toolbar.config.UPDATE_ON_FETCH }}">
    • {% trans "Hide" %} »
    • diff --git a/docs/changes.rst b/docs/changes.rst index a4a45afc0..92cce4fe6 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -6,7 +6,10 @@ Pending * Added hook to RedirectsPanel for subclass customization. * Added feature to sanitize sensitive data in the Request Panel. -* Fixed dark mode conflict in code block toolbar CSS +* Fixed dark mode conflict in code block toolbar CSS. +* Properly allowed overriding the system theme preference by using the theme + selector. Removed the ``DEFAULT_THEME`` setting, we should always default to + system-level defaults where possible. * Added support for using django-template-partials with the template panel's source view functionality. The same change possibly adds support for other template loaders. diff --git a/docs/configuration.rst b/docs/configuration.rst index 7cd6bc11b..d9e7ff342 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -201,13 +201,6 @@ Toolbar options request when it occurs. This is especially useful when using htmx boosting or similar JavaScript techniques. -.. _DEFAULT_THEME: - -* ``DEFAULT_THEME`` - - Default: ``"auto"`` - - This controls which theme will use the toolbar by default. Panel options ~~~~~~~~~~~~~ diff --git a/tests/test_integration.py b/tests/test_integration.py index 3cc0b1420..88cd8f9ab 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -649,6 +649,9 @@ def setUpClass(cls): options = Options() if os.environ.get("CI"): options.add_argument("-headless") + # Set the browser preference to light mode for consistent testing + options.set_preference("ui.systemUsesDarkTheme", 0) + options.set_preference("ui.prefersReducedMotion", 0) cls.selenium = webdriver.Firefox(options=options) @classmethod @@ -892,26 +895,35 @@ def test_theme_toggle(self): toolbar = self.selenium.find_element(By.ID, "djDebug") # Check that the default theme is auto - self.assertEqual(toolbar.get_attribute("data-theme"), "auto") + self.assertEqual(toolbar.get_attribute("data-user-theme"), "auto") # The theme toggle button is shown on the toolbar toggle_button = self.selenium.find_element(By.ID, "djToggleThemeButton") self.assertTrue(toggle_button.is_displayed()) - # The theme changes when user clicks the button - toggle_button.click() + # The browser is set to light mode via Firefox preferences + # With light mode system preference, the order is: auto -> dark -> light -> auto + # Check that auto initially uses light theme + self.assertEqual(toolbar.get_attribute("data-user-theme"), "auto") self.assertEqual(toolbar.get_attribute("data-theme"), "light") - toggle_button.click() + + # The theme changes when user clicks the button + toggle_button.click() # auto -> dark + self.assertEqual(toolbar.get_attribute("data-user-theme"), "dark") self.assertEqual(toolbar.get_attribute("data-theme"), "dark") - toggle_button.click() - self.assertEqual(toolbar.get_attribute("data-theme"), "auto") - # Switch back to light. - toggle_button.click() + + toggle_button.click() # dark -> light + self.assertEqual(toolbar.get_attribute("data-user-theme"), "light") + self.assertEqual(toolbar.get_attribute("data-theme"), "light") + + toggle_button.click() # light -> auto + self.assertEqual(toolbar.get_attribute("data-user-theme"), "auto") self.assertEqual(toolbar.get_attribute("data-theme"), "light") # Enter the page again to check that user settings is saved self.get("/regular/basic/") toolbar = self.selenium.find_element(By.ID, "djDebug") + self.assertEqual(toolbar.get_attribute("data-user-theme"), "auto") self.assertEqual(toolbar.get_attribute("data-theme"), "light") def test_async_sql_action(self): From 2a126e6bb9a3689e31711fb73c7d61313ffe4252 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Thu, 3 Apr 2025 16:11:24 +0200 Subject: [PATCH 183/200] Format the templates using djade (#2120) --- .pre-commit-config.yaml | 5 +++ .../templates/debug_toolbar/base.html | 12 +++---- .../debug_toolbar/includes/panel_button.html | 4 +-- .../debug_toolbar/panels/alerts.html | 4 +-- .../templates/debug_toolbar/panels/cache.html | 24 +++++++------- .../debug_toolbar/panels/headers.html | 20 ++++++------ .../debug_toolbar/panels/history.html | 14 ++++---- .../debug_toolbar/panels/history_tr.html | 4 +-- .../debug_toolbar/panels/profiling.html | 12 +++---- .../debug_toolbar/panels/request.html | 26 +++++++-------- .../panels/request_variables.html | 4 +-- .../debug_toolbar/panels/settings.html | 4 +-- .../debug_toolbar/panels/signals.html | 4 +-- .../templates/debug_toolbar/panels/sql.html | 32 +++++++++---------- .../debug_toolbar/panels/sql_explain.html | 8 ++--- .../debug_toolbar/panels/sql_profile.html | 10 +++--- .../debug_toolbar/panels/sql_select.html | 10 +++--- .../debug_toolbar/panels/staticfiles.html | 20 ++++++------ .../debug_toolbar/panels/template_source.html | 2 +- .../debug_toolbar/panels/templates.html | 16 +++++----- .../templates/debug_toolbar/panels/timer.html | 14 ++++---- .../debug_toolbar/panels/versions.html | 6 ++-- .../templates/debug_toolbar/redirect.html | 4 +-- docs/changes.rst | 2 ++ example/templates/htmx/boost.html | 2 +- example/templates/turbo/index.html | 2 +- tests/panels/test_sql.py | 4 +-- tests/templates/ajax/ajax.html | 3 +- tests/templates/basic.html | 1 + tests/templates/jinja2/basic.jinja | 3 +- tests/templates/sql/flat.html | 3 +- tests/templates/sql/nested.html | 3 +- tests/templates/staticfiles/async_static.html | 2 +- 33 files changed, 148 insertions(+), 136 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7978a3ba9..4dabe0de8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,11 @@ repos: hooks: - id: django-upgrade args: [--target-version, "4.2"] +- repo: https://github.com/adamchainz/djade-pre-commit + rev: "1.3.2" + hooks: + - id: djade + args: [--target-version, "4.2"] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index b5c225ac8..607863104 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -2,10 +2,10 @@ {% block css %} -{% endblock %} +{% endblock css %} {% block js %} -{% endblock %} +{% endblock js %}
      -
      +
      DJDT
      diff --git a/debug_toolbar/templates/debug_toolbar/includes/panel_button.html b/debug_toolbar/templates/debug_toolbar/includes/panel_button.html index 344331d8d..bc6f03ad9 100644 --- a/debug_toolbar/templates/debug_toolbar/includes/panel_button.html +++ b/debug_toolbar/templates/debug_toolbar/includes/panel_button.html @@ -1,7 +1,7 @@ {% load i18n %}
    • - + {% if panel.has_content and panel.enabled %} {% else %} @@ -9,7 +9,7 @@ {% endif %} {{ panel.nav_title }} {% if panel.enabled %} - {% with panel.nav_subtitle as subtitle %} + {% with subtitle=panel.nav_subtitle %} {% if subtitle %}
      {{ subtitle }}{% endif %} {% endwith %} {% endif %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/alerts.html b/debug_toolbar/templates/debug_toolbar/panels/alerts.html index df208836d..6665033fb 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/alerts.html +++ b/debug_toolbar/templates/debug_toolbar/panels/alerts.html @@ -1,12 +1,12 @@ {% load i18n %} {% if alerts %} -

      {% trans "Alerts found" %}

      +

      {% translate "Alerts found" %}

      {% for alert in alerts %}
      • {{ alert.alert }}
      {% endfor %} {% else %} -

      {% trans "No alerts found" %}

      +

      {% translate "No alerts found" %}

      {% endif %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/cache.html b/debug_toolbar/templates/debug_toolbar/panels/cache.html index 0e1ec2a4c..fe882750b 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/cache.html +++ b/debug_toolbar/templates/debug_toolbar/panels/cache.html @@ -1,12 +1,12 @@ {% load i18n %} -

      {% trans "Summary" %}

      +

      {% translate "Summary" %}

  • - {{ store_context.form }} + {{ store_context.form.as_div }}
    " + - stat.replace("Start", "") + - "" + - (performance.timing[stat] - timingOffset) + - " (+" + - (performance.timing[endStat] - performance.timing[stat]) + - ")${stat.replace("Start", "")}${elapsed} (+${duration})" + - stat + - "" + - (performance.timing[stat] - timingOffset) + - "${stat}${elapsed}
    - - - - + + + + @@ -18,7 +18,7 @@

    {% trans "Summary" %}

    {% trans "Total calls" %}{% trans "Total time" %}{% trans "Cache hits" %}{% trans "Cache misses" %}{% translate "Total calls" %}{% translate "Total time" %}{% translate "Cache hits" %}{% translate "Cache misses" %}
    -

    {% trans "Commands" %}

    +

    {% translate "Commands" %}

    @@ -36,15 +36,15 @@

    {% trans "Commands" %}

    {% if calls %} -

    {% trans "Calls" %}

    +

    {% translate "Calls" %}

    - - - - - + + + + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/headers.html b/debug_toolbar/templates/debug_toolbar/panels/headers.html index f4146e8dd..db33f1b59 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/headers.html +++ b/debug_toolbar/templates/debug_toolbar/panels/headers.html @@ -1,12 +1,12 @@ {% load i18n %} -

    {% trans "Request headers" %}

    +

    {% translate "Request headers" %}

    {% trans "Time (ms)" %}{% trans "Type" %}{% trans "Arguments" %}{% trans "Keyword arguments" %}{% trans "Backend" %}{% translate "Time (ms)" %}{% translate "Type" %}{% translate "Arguments" %}{% translate "Keyword arguments" %}{% translate "Backend" %}
    - - + + @@ -19,13 +19,13 @@

    {% trans "Request headers" %}

    {% trans "Key" %}{% trans "Value" %}{% translate "Key" %}{% translate "Value" %}
    -

    {% trans "Response headers" %}

    +

    {% translate "Response headers" %}

    - - + + @@ -38,15 +38,15 @@

    {% trans "Response headers" %}

    {% trans "Key" %}{% trans "Value" %}{% translate "Key" %}{% translate "Value" %}
    -

    {% trans "WSGI environ" %}

    +

    {% translate "WSGI environ" %}

    -

    {% trans "Since the WSGI environ inherits the environment of the server, only a significant subset is shown below." %}

    +

    {% translate "Since the WSGI environ inherits the environment of the server, only a significant subset is shown below." %}

    - - + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/history.html b/debug_toolbar/templates/debug_toolbar/panels/history.html index 840f6c9f4..ba7823d22 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/history.html +++ b/debug_toolbar/templates/debug_toolbar/panels/history.html @@ -1,4 +1,4 @@ -{% load i18n %}{% load static %} +{% load i18n static %} {{ refresh_form.as_div }} @@ -6,12 +6,12 @@
    {% trans "Key" %}{% trans "Value" %}{% translate "Key" %}{% translate "Value" %}
    - - - - - - + + + + + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/history_tr.html b/debug_toolbar/templates/debug_toolbar/panels/history_tr.html index eff544f1a..1642b4a47 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/history_tr.html +++ b/debug_toolbar/templates/debug_toolbar/panels/history_tr.html @@ -19,8 +19,8 @@ - - + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/profiling.html b/debug_toolbar/templates/debug_toolbar/panels/profiling.html index 4c1c3acd3..0c2206a13 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/profiling.html +++ b/debug_toolbar/templates/debug_toolbar/panels/profiling.html @@ -2,12 +2,12 @@
    {% trans "Time" %}{% trans "Method" %}{% trans "Path" %}{% trans "Request Variables" %}{% trans "Status" %}{% trans "Action" %}{% translate "Time" %}{% translate "Method" %}{% translate "Path" %}{% translate "Request Variables" %}{% translate "Status" %}{% translate "Action" %}
    {% trans "Variable" %}{% trans "Value" %}{% translate "Variable" %}{% translate "Value" %}
    - - - - - - + + + + + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/request.html b/debug_toolbar/templates/debug_toolbar/panels/request.html index 076d5f74f..4a16468b5 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/request.html +++ b/debug_toolbar/templates/debug_toolbar/panels/request.html @@ -1,13 +1,13 @@ {% load i18n %} -

    {% trans "View information" %}

    +

    {% translate "View information" %}

    {% trans "Call" %}{% trans "CumTime" %}{% trans "Per" %}{% trans "TotTime" %}{% trans "Per" %}{% trans "Count" %}{% translate "Call" %}{% translate "CumTime" %}{% translate "Per" %}{% translate "TotTime" %}{% translate "Per" %}{% translate "Count" %}
    - - - - + + + + @@ -21,29 +21,29 @@

    {% trans "View information" %}

    {% trans "View function" %}{% trans "Arguments" %}{% trans "Keyword arguments" %}{% trans "URL name" %}{% translate "View function" %}{% translate "Arguments" %}{% translate "Keyword arguments" %}{% translate "URL name" %}
    {% if cookies.list or cookies.raw %} -

    {% trans "Cookies" %}

    +

    {% translate "Cookies" %}

    {% include 'debug_toolbar/panels/request_variables.html' with variables=cookies %} {% else %} -

    {% trans "No cookies" %}

    +

    {% translate "No cookies" %}

    {% endif %} {% if session.list or session.raw %} -

    {% trans "Session data" %}

    +

    {% translate "Session data" %}

    {% include 'debug_toolbar/panels/request_variables.html' with variables=session %} {% else %} -

    {% trans "No session data" %}

    +

    {% translate "No session data" %}

    {% endif %} {% if get.list or get.raw %} -

    {% trans "GET data" %}

    +

    {% translate "GET data" %}

    {% include 'debug_toolbar/panels/request_variables.html' with variables=get %} {% else %} -

    {% trans "No GET data" %}

    +

    {% translate "No GET data" %}

    {% endif %} {% if post.list or post.raw %} -

    {% trans "POST data" %}

    +

    {% translate "POST data" %}

    {% include 'debug_toolbar/panels/request_variables.html' with variables=post %} {% else %} -

    {% trans "No POST data" %}

    +

    {% translate "No POST data" %}

    {% endif %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/request_variables.html b/debug_toolbar/templates/debug_toolbar/panels/request_variables.html index 92200f867..26b487ab0 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/request_variables.html +++ b/debug_toolbar/templates/debug_toolbar/panels/request_variables.html @@ -8,8 +8,8 @@ - {% trans "Variable" %} - {% trans "Value" %} + {% translate "Variable" %} + {% translate "Value" %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/settings.html b/debug_toolbar/templates/debug_toolbar/panels/settings.html index 14763e4e6..5214c1b42 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/settings.html +++ b/debug_toolbar/templates/debug_toolbar/panels/settings.html @@ -2,8 +2,8 @@ - - + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/signals.html b/debug_toolbar/templates/debug_toolbar/panels/signals.html index cd9f42c4a..abd648924 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/signals.html +++ b/debug_toolbar/templates/debug_toolbar/panels/signals.html @@ -2,8 +2,8 @@
    {% trans "Setting" %}{% trans "Value" %}{% translate "Setting" %}{% translate "Value" %}
    - - + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql.html b/debug_toolbar/templates/debug_toolbar/panels/sql.html index e5bf0b7f6..63cf293c1 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql.html @@ -3,15 +3,15 @@ {% for alias, info in databases %}
  • {{ alias }} - {{ info.time_spent|floatformat:"2" }} ms ({% blocktrans count info.num_queries as num %}{{ num }} query{% plural %}{{ num }} queries{% endblocktrans %} + {{ info.time_spent|floatformat:"2" }} ms ({% blocktranslate count num=info.num_queries %}{{ num }} query{% plural %}{{ num }} queries{% endblocktranslate %} {% if info.similar_count %} - {% blocktrans with count=info.similar_count trimmed %} + {% blocktranslate with count=info.similar_count trimmed %} including {{ count }} similar - {% endblocktrans %} + {% endblocktranslate %} {% if info.duplicate_count %} - {% blocktrans with dupes=info.duplicate_count trimmed %} + {% blocktranslate with dupes=info.duplicate_count trimmed %} and {{ dupes }} duplicates - {% endblocktrans %} + {% endblocktranslate %} {% endif %} {% endif %})
  • @@ -31,16 +31,16 @@ - - - - + + + + {% for query in queries %} - + @@ -49,13 +49,13 @@ {% if query.similar_count %} - {% blocktrans with count=query.similar_count %}{{ count }} similar queries.{% endblocktrans %} + {% blocktranslate with count=query.similar_count %}{{ count }} similar queries.{% endblocktranslate %} {% endif %} {% if query.duplicate_count %} - {% blocktrans with dupes=query.duplicate_count %}Duplicated {{ dupes }} times.{% endblocktrans %} + {% blocktranslate with dupes=query.duplicate_count %}Duplicated {{ dupes }} times.{% endblocktranslate %} {% endif %} @@ -92,12 +92,12 @@
    {% trans "Signal" %}{% trans "Receivers" %}{% translate "Signal" %}{% translate "Receivers" %}
    {% trans "Query" %}{% trans "Timeline" %}{% trans "Time (ms)" %}{% trans "Action" %}{% translate "Query" %}{% translate "Timeline" %}{% translate "Time (ms)" %}{% translate "Action" %}
    -

    {% trans "Connection:" %} {{ query.alias }}

    +

    {% translate "Connection:" %} {{ query.alias }}

    {% if query.iso_level %} -

    {% trans "Isolation level:" %} {{ query.iso_level }}

    +

    {% translate "Isolation level:" %} {{ query.iso_level }}

    {% endif %} {% if query.trans_status %} -

    {% trans "Transaction status:" %} {{ query.trans_status }}

    +

    {% translate "Transaction status:" %} {{ query.trans_status }}

    {% endif %} {% if query.stacktrace %}
    {{ query.stacktrace }}
    @@ -120,5 +120,5 @@
    {% else %} -

    {% trans "No SQL queries were recorded during this request." %}

    +

    {% translate "No SQL queries were recorded during this request." %}

    {% endif %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html b/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html index 61dadbda6..f169c838f 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html @@ -1,16 +1,16 @@ {% load i18n %}
    -

    {% trans "SQL explained" %}

    +

    {% translate "SQL explained" %}

    -
    {% trans "Executed SQL" %}
    +
    {% translate "Executed SQL" %}
    {{ sql|safe }}
    -
    {% trans "Time" %}
    +
    {% translate "Time" %}
    {{ duration }} ms
    -
    {% trans "Database" %}
    +
    {% translate "Database" %}
    {{ alias }}
    diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html b/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html index 57f20b619..6c07640e0 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html @@ -1,17 +1,17 @@ {% load i18n %}
    -

    {% trans "SQL profiled" %}

    +

    {% translate "SQL profiled" %}

    {% if result %}
    -
    {% trans "Executed SQL" %}
    +
    {% translate "Executed SQL" %}
    {{ sql|safe }}
    -
    {% trans "Time" %}
    +
    {% translate "Time" %}
    {{ duration }} ms
    -
    {% trans "Database" %}
    +
    {% translate "Database" %}
    {{ alias }}
    @@ -34,7 +34,7 @@

    {% trans "SQL profiled" %}

    {% else %}
    -
    {% trans "Error" %}
    +
    {% translate "Error" %}
    {{ result_error }}
    {% endif %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_select.html b/debug_toolbar/templates/debug_toolbar/panels/sql_select.html index 699c18d87..3667f8199 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql_select.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql_select.html @@ -1,16 +1,16 @@ {% load i18n %}
    -

    {% trans "SQL selected" %}

    +

    {% translate "SQL selected" %}

    -
    {% trans "Executed SQL" %}
    +
    {% translate "Executed SQL" %}
    {{ sql|safe }}
    -
    {% trans "Time" %}
    +
    {% translate "Time" %}
    {{ duration }} ms
    -
    {% trans "Database" %}
    +
    {% translate "Database" %}
    {{ alias }}
    {% if result %} @@ -33,7 +33,7 @@

    {% trans "SQL selected" %}

    {% else %} -

    {% trans "Empty set" %}

    +

    {% translate "Empty set" %}

    {% endif %}
    diff --git a/debug_toolbar/templates/debug_toolbar/panels/staticfiles.html b/debug_toolbar/templates/debug_toolbar/panels/staticfiles.html index 9aa519f67..aaa7c78ab 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/staticfiles.html +++ b/debug_toolbar/templates/debug_toolbar/panels/staticfiles.html @@ -1,17 +1,17 @@ {% load i18n %} -

    {% blocktrans count staticfiles_dirs|length as dirs_count %}Static file path{% plural %}Static file paths{% endblocktrans %}

    +

    {% blocktranslate count dirs_count=staticfiles_dirs|length %}Static file path{% plural %}Static file paths{% endblocktranslate %}

    {% if staticfiles_dirs %}
      {% for prefix, staticfiles_dir in staticfiles_dirs %} -
    1. {{ staticfiles_dir }}{% if prefix %} {% blocktrans %}(prefix {{ prefix }}){% endblocktrans %}{% endif %}
    2. +
    3. {{ staticfiles_dir }}{% if prefix %} {% blocktranslate %}(prefix {{ prefix }}){% endblocktranslate %}{% endif %}
    4. {% endfor %}
    {% else %} -

    {% trans "None" %}

    +

    {% translate "None" %}

    {% endif %} -

    {% blocktrans count staticfiles_apps|length as apps_count %}Static file app{% plural %}Static file apps{% endblocktrans %}

    +

    {% blocktranslate count apps_count=staticfiles_apps|length %}Static file app{% plural %}Static file apps{% endblocktranslate %}

    {% if staticfiles_apps %}
      {% for static_app in staticfiles_apps %} @@ -19,10 +19,10 @@

      {% blocktrans count staticfiles_apps|length as apps_count %}Static file app{ {% endfor %}

    {% else %} -

    {% trans "None" %}

    +

    {% translate "None" %}

    {% endif %} -

    {% blocktrans count staticfiles|length as staticfiles_count %}Static file{% plural %}Static files{% endblocktrans %}

    +

    {% blocktranslate count staticfiles_count=staticfiles|length %}Static file{% plural %}Static files{% endblocktranslate %}

    {% if staticfiles %}
    {% for staticfile in staticfiles %} @@ -31,17 +31,17 @@

    {% blocktrans count staticfiles|length as staticfiles_count %}Static file{% {% endfor %}

    {% else %} -

    {% trans "None" %}

    +

    {% translate "None" %}

    {% endif %} {% for finder, payload in staticfiles_finders.items %} -

    {{ finder }} ({% blocktrans count payload|length as payload_count %}{{ payload_count }} file{% plural %}{{ payload_count }} files{% endblocktrans %})

    +

    {{ finder }} ({% blocktranslate count payload_count=payload|length %}{{ payload_count }} file{% plural %}{{ payload_count }} files{% endblocktranslate %})

    - - + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/template_source.html b/debug_toolbar/templates/debug_toolbar/panels/template_source.html index 397c44b24..6e52a6ab8 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/template_source.html +++ b/debug_toolbar/templates/debug_toolbar/panels/template_source.html @@ -1,7 +1,7 @@ {% load i18n %}
    -

    {% trans "Template source:" %} {{ template_name }}

    +

    {% translate "Template source:" %} {{ template_name }}

    diff --git a/debug_toolbar/templates/debug_toolbar/panels/templates.html b/debug_toolbar/templates/debug_toolbar/panels/templates.html index 121c086a8..4ceae12e7 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/templates.html +++ b/debug_toolbar/templates/debug_toolbar/panels/templates.html @@ -1,5 +1,5 @@ {% load i18n %} -

    {% blocktrans count template_dirs|length as template_count %}Template path{% plural %}Template paths{% endblocktrans %}

    +

    {% blocktranslate count template_count=template_dirs|length %}Template path{% plural %}Template paths{% endblocktranslate %}

    {% if template_dirs %}
      {% for template in template_dirs %} @@ -7,10 +7,10 @@

      {% blocktrans count template_dirs|length as template_count %}Template path{% {% endfor %}

    {% else %} -

    {% trans "None" %}

    +

    {% translate "None" %}

    {% endif %} -

    {% blocktrans count templates|length as template_count %}Template{% plural %}Templates{% endblocktrans %}

    +

    {% blocktranslate count template_count=templates|length %}Template{% plural %}Templates{% endblocktranslate %}

    {% if templates %}
    {% for template in templates %} @@ -19,7 +19,7 @@

    {% blocktrans count templates|length as template_count %}Template{% plural % {% if template.context %}
    - {% trans "Toggle context" %} + {% translate "Toggle context" %} {{ template.context }}
    @@ -27,22 +27,22 @@

    {% blocktrans count templates|length as template_count %}Template{% plural % {% endfor %}

    {% else %} -

    {% trans "None" %}

    +

    {% translate "None" %}

    {% endif %} -

    {% blocktrans count context_processors|length as context_processors_count %}Context processor{% plural %}Context processors{% endblocktrans %}

    +

    {% blocktranslate count context_processors_count=context_processors|length %}Context processor{% plural %}Context processors{% endblocktranslate %}

    {% if context_processors %}
    {% for key, value in context_processors.items %}
    {{ key|escape }}
    - {% trans "Toggle context" %} + {% translate "Toggle context" %} {{ value|escape }}
    {% endfor %}
    {% else %} -

    {% trans "None" %}

    +

    {% translate "None" %}

    {% endif %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/timer.html b/debug_toolbar/templates/debug_toolbar/panels/timer.html index 11483c107..b85720483 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/timer.html +++ b/debug_toolbar/templates/debug_toolbar/panels/timer.html @@ -1,5 +1,5 @@ {% load i18n %} -

    {% trans "Resource usage" %}

    +

    {% translate "Resource usage" %}

    {% trans 'Path' %}{% trans 'Location' %}{% translate 'Path' %}{% translate 'Location' %}
    @@ -7,8 +7,8 @@

    {% trans "Resource usage" %}

    - - + + @@ -23,7 +23,7 @@

    {% trans "Resource usage" %}

    -

    {% trans "Browser timing" %}

    +

    {% translate "Browser timing" %}

    {% trans "Resource" %}{% trans "Value" %}{% translate "Resource" %}{% translate "Value" %}
    @@ -32,9 +32,9 @@

    {% trans "Browser timing" %}

    - - - + + + diff --git a/debug_toolbar/templates/debug_toolbar/panels/versions.html b/debug_toolbar/templates/debug_toolbar/panels/versions.html index d0ade6cfb..3428c0561 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/versions.html +++ b/debug_toolbar/templates/debug_toolbar/panels/versions.html @@ -7,9 +7,9 @@ - - - + + + diff --git a/debug_toolbar/templates/debug_toolbar/redirect.html b/debug_toolbar/templates/debug_toolbar/redirect.html index 9d8966ed7..46897846d 100644 --- a/debug_toolbar/templates/debug_toolbar/redirect.html +++ b/debug_toolbar/templates/debug_toolbar/redirect.html @@ -7,9 +7,9 @@

    {{ status_line }}

    -

    {% trans "Location:" %} {{ redirect_to }}

    +

    {% translate "Location:" %} {{ redirect_to }}

    - {% trans "The Django Debug Toolbar has intercepted a redirect to the above URL for debug viewing purposes. You can click the above link to continue with the redirect as normal." %} + {% translate "The Django Debug Toolbar has intercepted a redirect to the above URL for debug viewing purposes. You can click the above link to continue with the redirect as normal." %}

    diff --git a/docs/changes.rst b/docs/changes.rst index 92cce4fe6..9c9195623 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -13,6 +13,8 @@ Pending * Added support for using django-template-partials with the template panel's source view functionality. The same change possibly adds support for other template loaders. +* Introduced `djade `__ to format Django + templates. 5.1.0 (2025-03-20) ------------------ diff --git a/example/templates/htmx/boost.html b/example/templates/htmx/boost.html index 782303b4e..7153a79ee 100644 --- a/example/templates/htmx/boost.html +++ b/example/templates/htmx/boost.html @@ -7,7 +7,7 @@ -

    Index of Tests (htmx) - Page {{page_num|default:"1"}}

    +

    Index of Tests (htmx) - Page {{ page_num|default:"1" }}

    For the debug panel to remain through page navigation, add the setting: diff --git a/example/templates/turbo/index.html b/example/templates/turbo/index.html index 143054e37..16ca9f2c6 100644 --- a/example/templates/turbo/index.html +++ b/example/templates/turbo/index.html @@ -7,7 +7,7 @@ -

    Turbo Index - Page {{page_num|default:"1"}}

    +

    Turbo Index - Page {{ page_num|default:"1" }}

    For the debug panel to remain through page navigation, add the setting: diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py index 8e105657b..a411abb5d 100644 --- a/tests/panels/test_sql.py +++ b/tests/panels/test_sql.py @@ -655,8 +655,8 @@ def test_flat_template_information(self): template_info = query["template_info"] template_name = os.path.basename(template_info["name"]) self.assertEqual(template_name, "flat.html") - self.assertEqual(template_info["context"][2]["content"].strip(), "{{ users }}") - self.assertEqual(template_info["context"][2]["highlight"], True) + self.assertEqual(template_info["context"][3]["content"].strip(), "{{ users }}") + self.assertEqual(template_info["context"][3]["highlight"], True) @override_settings( DEBUG=True, diff --git a/tests/templates/ajax/ajax.html b/tests/templates/ajax/ajax.html index c9de3acb6..7955456de 100644 --- a/tests/templates/ajax/ajax.html +++ b/tests/templates/ajax/ajax.html @@ -1,4 +1,5 @@ {% extends "base.html" %} + {% block content %}

    click for ajax
    @@ -18,4 +19,4 @@ } document.addEventListener("click", (event) => {send_ajax()}); -{% endblock %} +{% endblock content %} diff --git a/tests/templates/basic.html b/tests/templates/basic.html index 46f88e4da..02f87200a 100644 --- a/tests/templates/basic.html +++ b/tests/templates/basic.html @@ -1,2 +1,3 @@ {% extends "base.html" %} + {% block content %}Test for {{ title }}{% endblock %} diff --git a/tests/templates/jinja2/basic.jinja b/tests/templates/jinja2/basic.jinja index e531eee64..1ebced724 100644 --- a/tests/templates/jinja2/basic.jinja +++ b/tests/templates/jinja2/basic.jinja @@ -1,5 +1,6 @@ {% extends 'base.html' %} + {% block content %} Test for {{ title }} (Jinja) {% for i in range(10) %}{{ i }}{% endfor %} {# Jinja2 supports range(), Django templates do not #} -{% endblock %} +{% endblock content %} diff --git a/tests/templates/sql/flat.html b/tests/templates/sql/flat.html index 058dbe043..ee5386c55 100644 --- a/tests/templates/sql/flat.html +++ b/tests/templates/sql/flat.html @@ -1,4 +1,5 @@ {% extends "base.html" %} + {% block content %} {{ users }} -{% endblock %} +{% endblock content %} diff --git a/tests/templates/sql/nested.html b/tests/templates/sql/nested.html index 8558e2d45..e23a53af1 100644 --- a/tests/templates/sql/nested.html +++ b/tests/templates/sql/nested.html @@ -1,4 +1,5 @@ {% extends "base.html" %} + {% block content %} {% include "sql/included.html" %} -{% endblock %} +{% endblock content %} diff --git a/tests/templates/staticfiles/async_static.html b/tests/templates/staticfiles/async_static.html index fc0c9b885..80f636cce 100644 --- a/tests/templates/staticfiles/async_static.html +++ b/tests/templates/staticfiles/async_static.html @@ -3,4 +3,4 @@ {% block head %} -{% endblock %} +{% endblock head %} From 347c9e12775609e401ccd62ea9c8d3af3ad05385 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:13:55 +0200 Subject: [PATCH 184/200] [pre-commit.ci] pre-commit autoupdate (#2122) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4dabe0de8..345146492 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.2' + rev: 'v0.11.4' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 2eca4f77ef9eca8cdfff1bcb870bd52292f40997 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:09:11 +0200 Subject: [PATCH 185/200] [pre-commit.ci] pre-commit autoupdate (#2126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/adamchainz/djade-pre-commit: 1.3.2 → 1.4.0](https://github.com/adamchainz/djade-pre-commit/compare/1.3.2...1.4.0) - [github.com/biomejs/pre-commit: v1.9.4 → v2.0.0-beta.1](https://github.com/biomejs/pre-commit/compare/v1.9.4...v2.0.0-beta.1) - [github.com/astral-sh/ruff-pre-commit: v0.11.4 → v0.11.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.4...v0.11.6) * Update .pre-commit-config.yaml --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Matthias Kestenholz --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 345146492..f4ba4b2f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: django-upgrade args: [--target-version, "4.2"] - repo: https://github.com/adamchainz/djade-pre-commit - rev: "1.3.2" + rev: "1.4.0" hooks: - id: djade args: [--target-version, "4.2"] @@ -34,7 +34,7 @@ repos: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.4' + rev: 'v0.11.6' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 7522214e69e0d9ea0ecf6669b16c79e757646305 Mon Sep 17 00:00:00 2001 From: Tom Hall <72264908+TomHall2020@users.noreply.github.com> Date: Sun, 27 Apr 2025 08:37:20 +0100 Subject: [PATCH 186/200] Keep panel close button accessible with external styles (#2128) Swapped order of header and button HTML elements to prevent button becoming inaccessible with external CSS --- debug_toolbar/static/debug_toolbar/js/utils.js | 2 +- .../templates/debug_toolbar/includes/panel_content.html | 2 +- debug_toolbar/templates/debug_toolbar/panels/sql_explain.html | 2 +- debug_toolbar/templates/debug_toolbar/panels/sql_profile.html | 2 +- debug_toolbar/templates/debug_toolbar/panels/sql_select.html | 2 +- .../templates/debug_toolbar/panels/template_source.html | 2 +- docs/changes.rst | 2 ++ tests/panels/test_custom.py | 2 +- tests/panels/test_settings.py | 2 +- tests/test_integration.py | 2 +- 10 files changed, 11 insertions(+), 9 deletions(-) diff --git a/debug_toolbar/static/debug_toolbar/js/utils.js b/debug_toolbar/static/debug_toolbar/js/utils.js index c42963fe3..0cfa80474 100644 --- a/debug_toolbar/static/debug_toolbar/js/utils.js +++ b/debug_toolbar/static/debug_toolbar/js/utils.js @@ -90,7 +90,7 @@ function ajax(url, init) { }) .catch((error) => { const win = document.getElementById("djDebugWindow"); - win.innerHTML = `

    ${error.message}

    `; + win.innerHTML = `

    ${error.message}

    `; $$.show(win); throw error; }); diff --git a/debug_toolbar/templates/debug_toolbar/includes/panel_content.html b/debug_toolbar/templates/debug_toolbar/includes/panel_content.html index b0f4af704..d797421a5 100644 --- a/debug_toolbar/templates/debug_toolbar/includes/panel_content.html +++ b/debug_toolbar/templates/debug_toolbar/includes/panel_content.html @@ -3,8 +3,8 @@ {% if panel.has_content and panel.enabled %}
    -

    {{ panel.title }}

    +
    {% if toolbar.should_render_panels %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html b/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html index f169c838f..b9ff2911d 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html @@ -1,7 +1,7 @@ {% load i18n %}
    -

    {% translate "SQL explained" %}

    +
    diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html b/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html index 6c07640e0..d18a309c6 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql_profile.html @@ -1,7 +1,7 @@ {% load i18n %}
    -

    {% translate "SQL profiled" %}

    +
    diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql_select.html b/debug_toolbar/templates/debug_toolbar/panels/sql_select.html index 3667f8199..9360cde05 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql_select.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql_select.html @@ -1,7 +1,7 @@ {% load i18n %}
    -

    {% translate "SQL selected" %}

    +
    diff --git a/debug_toolbar/templates/debug_toolbar/panels/template_source.html b/debug_toolbar/templates/debug_toolbar/panels/template_source.html index 6e52a6ab8..4d47fd3c3 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/template_source.html +++ b/debug_toolbar/templates/debug_toolbar/panels/template_source.html @@ -1,7 +1,7 @@ {% load i18n %}
    -

    {% translate "Template source:" %} {{ template_name }}

    +
    diff --git a/docs/changes.rst b/docs/changes.rst index 9c9195623..e7cdbef0e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -15,6 +15,8 @@ Pending template loaders. * Introduced `djade `__ to format Django templates. +* Swapped display order of panel header and close button to prevent style + conflicts 5.1.0 (2025-03-20) ------------------ diff --git a/tests/panels/test_custom.py b/tests/panels/test_custom.py index f13c4ef62..661a5cc53 100644 --- a/tests/panels/test_custom.py +++ b/tests/panels/test_custom.py @@ -33,8 +33,8 @@ def test_escapes_panel_title(self): """
    -

    Title with special chars &"'<>

    +
    diff --git a/tests/panels/test_settings.py b/tests/panels/test_settings.py index 5bf29d322..89b016dc0 100644 --- a/tests/panels/test_settings.py +++ b/tests/panels/test_settings.py @@ -24,8 +24,8 @@ def test_panel_title(self): """
    -

    Settings from None

    +
    diff --git a/tests/test_integration.py b/tests/test_integration.py index 88cd8f9ab..a431ba29f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -812,7 +812,7 @@ def test_displays_server_error(self): debug_window = self.selenium.find_element(By.ID, "djDebugWindow") self.selenium.find_element(By.CLASS_NAME, "BuggyPanel").click() self.wait.until(EC.visibility_of(debug_window)) - self.assertEqual(debug_window.text, "»\n500: Internal Server Error") + self.assertEqual(debug_window.text, "500: Internal Server Error\n»") def test_toolbar_language_will_render_to_default_language_when_not_set(self): self.get("/regular/basic/") From 8d31b2decb28a5382f870fb516e6fee217672a6c Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Tue, 29 Apr 2025 06:27:18 +0200 Subject: [PATCH 187/200] Add CSS resets for height and min-height (#2130) Refs https://github.com/django/djangoproject.com/pull/2041. --- debug_toolbar/static/debug_toolbar/css/toolbar.css | 4 +++- docs/changes.rst | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index e47dcc975..f147bcdff 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -127,8 +127,10 @@ #djDebug button { margin: 0; padding: 0; - min-width: 0; + min-width: auto; width: auto; + min-height: auto; + height: auto; border: 0; outline: 0; font-size: 12px; diff --git a/docs/changes.rst b/docs/changes.rst index e7cdbef0e..8b32c52da 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -17,6 +17,8 @@ Pending templates. * Swapped display order of panel header and close button to prevent style conflicts +* Added CSS for resetting the height of elements too to avoid problems with + global CSS of a website where the toolbar is used. 5.1.0 (2025-03-20) ------------------ From db645c2571ee6e061b5fac401a9d49e5844820c0 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Tue, 29 Apr 2025 07:17:29 +0200 Subject: [PATCH 188/200] Update ruff --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4ba4b2f9..852048216 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.6' + rev: 'v0.11.7' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From e5f0ccd99ac8bbba184295b7a881b350cd911436 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Tue, 29 Apr 2025 07:21:20 +0200 Subject: [PATCH 189/200] django-debug-toolbar 5.2 --- README.rst | 2 +- debug_toolbar/__init__.py | 2 +- docs/changes.rst | 4 ++++ docs/conf.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 3c831efa7..6c7da3615 100644 --- a/README.rst +++ b/README.rst @@ -40,7 +40,7 @@ Here's a screenshot of the toolbar in action: In addition to the built-in panels, a number of third-party panels are contributed by the community. -The current stable version of the Debug Toolbar is 5.1.0. It works on +The current stable version of the Debug Toolbar is 5.2.0. It works on Django ≥ 4.2.0. The Debug Toolbar has experimental support for `Django's asynchronous views diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py index 5bdaa2dd1..770c5eeed 100644 --- a/debug_toolbar/__init__.py +++ b/debug_toolbar/__init__.py @@ -4,7 +4,7 @@ # Do not use pkg_resources to find the version but set it here directly! # see issue #1446 -VERSION = "5.1.0" +VERSION = "5.2.0" # Code that discovers files or modules in INSTALLED_APPS imports this module. urls = "debug_toolbar.urls", APP_NAME diff --git a/docs/changes.rst b/docs/changes.rst index 8b32c52da..bf1998de8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,9 @@ Change log Pending ------- +5.2.0 (2025-04-29) +------------------ + * Added hook to RedirectsPanel for subclass customization. * Added feature to sanitize sensitive data in the Request Panel. * Fixed dark mode conflict in code block toolbar CSS. @@ -22,6 +25,7 @@ Pending 5.1.0 (2025-03-20) ------------------ + * Added Django 5.2 to the tox matrix. * Updated package metadata to include well-known labels. * Added resources section to the documentation. diff --git a/docs/conf.py b/docs/conf.py index 4cb37988e..6e67aac2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ copyright = copyright.format(datetime.date.today().year) # The full version, including alpha/beta/rc tags -release = "5.1.0" +release = "5.2.0" # -- General configuration --------------------------------------------------- From bbd3cff801a8122828e1bcf4242b605aced3c73b Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Tue, 29 Apr 2025 07:27:47 +0200 Subject: [PATCH 190/200] Add the GitHub release to the releasing process and remove the RTD notice, the default behavior is fine there --- docs/contributing.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 4d690c954..1ab7077aa 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -199,7 +199,8 @@ The release itself requires the following steps: #. Push the commit and the tag. -#. Publish the release from the Django Commons website. +#. Publish the release from the GitHub actions workflow. -#. Change the default version of the docs to point to the latest release: - https://readthedocs.org/dashboard/django-debug-toolbar/versions/ +#. **After the publishing completed** edit the automatically created GitHub + release to include the release notes (you may use GitHub's "Generate release + notes" button for this). From f00bfb667bf311c32db34e5e838b0b02e587012a Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Sat, 3 May 2025 00:46:48 -0500 Subject: [PATCH 191/200] Remove pin for django-csp. (#2132) * Remove pin for django-csp. The toolbar now supports django-csp v4 * Add django-template-partials to requirements file This allows make test to succeed again. --- requirements_dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 941e74a81..6915226fd 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -11,7 +11,8 @@ html5lib selenium tox black -django-csp<4 # Used in tests/test_csp_rendering +django-template-partials +django-csp # Used in tests/test_csp_rendering # Integration support From 3d91e92592896ee090a27fc465d0e00be1c7fc1e Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Mon, 5 May 2025 08:23:59 +0200 Subject: [PATCH 192/200] Update the biome pre-commit hook now that biome doesn't crash on Django HTML templates anymore --- .pre-commit-config.yaml | 4 +-- biome.json | 36 ++++++++++++++++--- .../static/debug_toolbar/css/toolbar.css | 4 ++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 852048216..dc2a7b2e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,12 +29,12 @@ repos: - id: rst-backticks - id: rst-directive-colons - repo: https://github.com/biomejs/pre-commit - rev: v1.9.4 + rev: v2.0.0-beta.2 hooks: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.7' + rev: 'v0.11.8' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/biome.json b/biome.json index 625e4ebe7..5cf9a9c90 100644 --- a/biome.json +++ b/biome.json @@ -1,16 +1,44 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.0.0-beta.2/schema.json", "formatter": { "enabled": true, "useEditorconfig": true }, - "organizeImports": { - "enabled": true + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } }, "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "style": { + "useLiteralEnumMembers": "error", + "noCommaOperator": "error", + "useNodejsImportProtocol": "error", + "useAsConstAssertion": "error", + "useNumericLiterals": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useConst": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "useExponentiationOperator": "error", + "useTemplate": "error", + "noParameterAssign": "error", + "noNonNullAssertion": "error", + "useDefaultParameterLast": "error", + "noArguments": "error", + "useImportType": "error", + "useExportType": "error", + "noUselessElse": "error", + "useShorthandFunctionType": "error" + } } }, "javascript": { diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index f147bcdff..3a8d5628f 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -170,7 +170,9 @@ #djDebug button:active { border: 1px solid #aaa; border-bottom: 1px solid #888; - box-shadow: inset 0 0 5px 2px #aaa, 0 1px 0 0 #eee; + box-shadow: + inset 0 0 5px 2px #aaa, + 0 1px 0 0 #eee; } #djDebug #djDebugToolbar { From fbceff0de2da81cab4a025cf7cbded345ef207cf Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Mon, 12 May 2025 08:02:56 +0200 Subject: [PATCH 193/200] Build docs on Ubuntu 24.04 --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5843d0212..794f8b3ed 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,7 +5,7 @@ version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: python: "3.10" From cf71ded725ddda6124e55762cc2115567d6eec4c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 22:06:03 +0200 Subject: [PATCH 194/200] [pre-commit.ci] pre-commit autoupdate (#2135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/biomejs/pre-commit: v2.0.0-beta.2 → v2.0.0-beta.3](https://github.com/biomejs/pre-commit/compare/v2.0.0-beta.2...v2.0.0-beta.3) - [github.com/astral-sh/ruff-pre-commit: v0.11.8 → v0.11.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.8...v0.11.9) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc2a7b2e6..371824a6b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,12 +29,12 @@ repos: - id: rst-backticks - id: rst-directive-colons - repo: https://github.com/biomejs/pre-commit - rev: v2.0.0-beta.2 + rev: v2.0.0-beta.3 hooks: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.8' + rev: 'v0.11.9' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 11c321f0670dcedd74423bf0407bc95c217f516d Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Fri, 16 May 2025 15:58:35 -0500 Subject: [PATCH 195/200] Disabled document.cookie linter The replacement, CookieStore isn't accessible on all browsers at this time, nor should we assume we always have access to secure environments. --- biome.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/biome.json b/biome.json index 5cf9a9c90..75ad07db8 100644 --- a/biome.json +++ b/biome.json @@ -38,6 +38,9 @@ "useExportType": "error", "noUselessElse": "error", "useShorthandFunctionType": "error" + }, + "suspicious": { + "noDocumentCookie": "off" } } }, From 7d68e0e7b61eb5133144c6f2cdb2d70c7b00d3a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 06:50:01 +0200 Subject: [PATCH 196/200] [pre-commit.ci] pre-commit autoupdate (#2140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/adamchainz/django-upgrade: 1.24.0 → 1.25.0](https://github.com/adamchainz/django-upgrade/compare/1.24.0...1.25.0) - [github.com/biomejs/pre-commit: v2.0.0-beta.3 → v2.0.0-beta.4](https://github.com/biomejs/pre-commit/compare/v2.0.0-beta.3...v2.0.0-beta.4) - [github.com/astral-sh/ruff-pre-commit: v0.11.9 → v0.11.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.9...v0.11.10) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 371824a6b..c178042a1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/adamchainz/django-upgrade - rev: 1.24.0 + rev: 1.25.0 hooks: - id: django-upgrade args: [--target-version, "4.2"] @@ -29,12 +29,12 @@ repos: - id: rst-backticks - id: rst-directive-colons - repo: https://github.com/biomejs/pre-commit - rev: v2.0.0-beta.3 + rev: v2.0.0-beta.4 hooks: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.9' + rev: 'v0.11.10' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 46e2b9183572d94a5695078efbaec0314556a82d Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Fri, 23 May 2025 03:00:01 -0500 Subject: [PATCH 197/200] Added check for pytest as test runner for IS_RUNNING_TESTS. (#2137) * Added check for pytest as test runner for IS_RUNNING_TESTS. * Move logic to determine IS_RUNNING_TESTS to a function This makes the logic testable. * Added words to spelling list for the changelog. --- debug_toolbar/settings.py | 12 +++++++++++- docs/changes.rst | 3 +++ docs/configuration.rst | 2 +- docs/installation.rst | 2 +- docs/spelling_wordlist.txt | 2 ++ tests/test_settings.py | 22 ++++++++++++++++++++++ 6 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 tests/test_settings.py diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index 59d538a0b..4dc801c2c 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -1,3 +1,4 @@ +import os import sys import warnings from functools import cache @@ -6,6 +7,15 @@ from django.dispatch import receiver from django.test.signals import setting_changed + +def _is_running_tests(): + """ + Helper function to support testing default value for + IS_RUNNING_TESTS + """ + return "test" in sys.argv or "PYTEST_VERSION" in os.environ + + CONFIG_DEFAULTS = { # Toolbar options "DISABLE_PANELS": { @@ -43,7 +53,7 @@ "SQL_WARNING_THRESHOLD": 500, # milliseconds "OBSERVE_REQUEST_CALLBACK": "debug_toolbar.toolbar.observe_request", "TOOLBAR_LANGUAGE": None, - "IS_RUNNING_TESTS": "test" in sys.argv, + "IS_RUNNING_TESTS": _is_running_tests(), "UPDATE_ON_FETCH": False, } diff --git a/docs/changes.rst b/docs/changes.rst index bf1998de8..6d6f34b2d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,9 @@ Change log Pending ------- +* Added support for checking if pytest as the test runner when determining + if tests are running. + 5.2.0 (2025-04-29) ------------------ diff --git a/docs/configuration.rst b/docs/configuration.rst index d9e7ff342..377c97da8 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -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 diff --git a/docs/installation.rst b/docs/installation.rst index 61187570d..b89a2f563 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -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 = [ diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 0f58c1f52..79b05cb06 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -49,6 +49,7 @@ psycopg py pyflame pylibmc +pytest pyupgrade querysets refactoring @@ -65,5 +66,6 @@ theming timeline tox uWSGI +unhandled unhashable validator diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 000000000..f7dc676b1 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,22 @@ +from unittest.mock import patch + +from django.test import TestCase + +from debug_toolbar.settings import _is_running_tests + + +class SettingsTestCase(TestCase): + @patch("debug_toolbar.settings.sys") + @patch("debug_toolbar.settings.os") + def test_is_running_tests(self, mock_os, mock_sys): + mock_sys.argv = "test" + mock_os.environ = {} + self.assertTrue(_is_running_tests()) + + mock_sys.argv = "" + mock_os.environ = {} + self.assertFalse(_is_running_tests()) + + mock_sys.argv = "" + mock_os.environ = {"PYTEST_VERSION": "1"} + self.assertTrue(_is_running_tests()) From 3fc0c05f15fb060c2132176339a9ec00d8f66c60 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 20:03:53 +0000 Subject: [PATCH 198/200] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/biomejs/pre-commit: v2.0.0-beta.4 → v2.0.0-beta.5](https://github.com/biomejs/pre-commit/compare/v2.0.0-beta.4...v2.0.0-beta.5) - [github.com/astral-sh/ruff-pre-commit: v0.11.10 → v0.11.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.10...v0.11.11) - [github.com/tox-dev/pyproject-fmt: v2.5.1 → v2.6.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.5.1...v2.6.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c178042a1..fae72da9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,18 +29,18 @@ repos: - id: rst-backticks - id: rst-directive-colons - repo: https://github.com/biomejs/pre-commit - rev: v2.0.0-beta.4 + rev: v2.0.0-beta.5 hooks: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.10' + rev: 'v0.11.11' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.1 + rev: v2.6.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject From 5054a2382e124bc82e72abd347b0ce9f8b1c43d8 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Tue, 27 May 2025 07:47:59 +0200 Subject: [PATCH 199/200] Update the biome configuration --- biome.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/biome.json b/biome.json index 75ad07db8..dc2776d79 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.0.0-beta.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.0.0-beta.5/schema.json", "formatter": { "enabled": true, "useEditorconfig": true @@ -20,7 +20,6 @@ "noCommaOperator": "error", "useNodejsImportProtocol": "error", "useAsConstAssertion": "error", - "useNumericLiterals": "error", "useEnumInitializers": "error", "useSelfClosingElements": "error", "useConst": "error", @@ -41,6 +40,9 @@ }, "suspicious": { "noDocumentCookie": "off" + }, + "complexity": { + "useNumericLiterals": "error" } } }, From c217334010fa54a8726640519ca29cb77fffda58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:35:54 +0200 Subject: [PATCH 200/200] [pre-commit.ci] pre-commit autoupdate (#2142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.11 → v0.11.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.11...v0.11.12) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fae72da9c..8c6115813 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: biome-check verbose: true - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.11.11' + rev: 'v0.11.12' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix]
    {% trans "Timing attribute" %}{% trans "Timeline" %}{% trans "Milliseconds since navigation start (+length)" %}{% translate "Timing attribute" %}{% translate "Timeline" %}{% translate "Milliseconds since navigation start (+length)" %}
    {% trans "Package" %}{% trans "Name" %}{% trans "Version" %}{% translate "Package" %}{% translate "Name" %}{% translate "Version" %}