From a0f0df97d6b366fb7179dcb03183983cae25efa8 Mon Sep 17 00:00:00 2001
From: Federico Bond
Date: Sat, 1 Jun 2024 08:49:07 -0300
Subject: [PATCH 01/12] Fix overriding font-family for both light and dark
themes (#1930)
---
debug_toolbar/static/debug_toolbar/css/toolbar.css | 6 ++++--
docs/changes.rst | 2 ++
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css
index e028a67b7..c7e201d07 100644
--- a/debug_toolbar/static/debug_toolbar/css/toolbar.css
+++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css
@@ -1,6 +1,5 @@
/* Variable definitions */
-:root,
-#djDebug[data-theme="light"] {
+:root {
/* 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",
@@ -10,7 +9,10 @@
"Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New",
monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
+}
+:root,
+#djDebug[data-theme="light"] {
--djdt-font-color: black;
--djdt-background-color: white;
--djdt-panel-content-background-color: #eee;
diff --git a/docs/changes.rst b/docs/changes.rst
index a0215d09c..20272fda7 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,6 +4,8 @@ Change log
Pending
-------
+* Fix overriding font-family for both light and dark themes.
+
4.4.2 (2024-05-27)
------------------
From cf0cc0dfe86e134ea2a0352a4f0833efbcf9f2c4 Mon Sep 17 00:00:00 2001
From: Ceesjan Luiten
Date: Sat, 1 Jun 2024 13:49:49 +0200
Subject: [PATCH 02/12] Restore compatibility with iptools.IpRangeList (#1929)
---
debug_toolbar/middleware.py | 15 ++++++++++++---
tests/test_integration.py | 22 ++++++++++++++++++++++
2 files changed, 34 insertions(+), 3 deletions(-)
diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py
index 0513e2379..65b5282c5 100644
--- a/debug_toolbar/middleware.py
+++ b/debug_toolbar/middleware.py
@@ -20,8 +20,14 @@ def show_toolbar(request):
"""
Default function to determine whether to show the toolbar on a given page.
"""
- internal_ips = list(settings.INTERNAL_IPS)
+ if not settings.DEBUG:
+ return False
+ # Test: settings
+ if request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS:
+ return True
+
+ # Test: Docker
try:
# This is a hack for docker installations. It attempts to look
# up the IP address of the docker host.
@@ -31,11 +37,14 @@ def show_toolbar(request):
".".join(socket.gethostbyname("host.docker.internal").rsplit(".")[:-1])
+ ".1"
)
- internal_ips.append(docker_ip)
+ if request.META.get("REMOTE_ADDR") == docker_ip:
+ return True
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
+
+ # No test passed
+ return False
@lru_cache(maxsize=None)
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 465804651..71525affe 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -75,6 +75,28 @@ def test_show_toolbar_docker(self, mocked_gethostbyname):
self.assertTrue(show_toolbar(self.request))
mocked_gethostbyname.assert_called_once_with("host.docker.internal")
+ def test_not_iterating_over_INTERNAL_IPS(self):
+ """Verify that the middleware does not iterate over INTERNAL_IPS in some way.
+
+ Some people use iptools.IpRangeList for their INTERNAL_IPS. This is a class
+ that can quickly answer the question if the setting contain a certain IP address,
+ but iterating over this object will drain all performance / blow up.
+ """
+
+ class FailOnIteration:
+ def __iter__(self):
+ raise RuntimeError(
+ "The testcase failed: the code should not have iterated over INTERNAL_IPS"
+ )
+
+ def __contains__(self, x):
+ return True
+
+ with self.settings(INTERNAL_IPS=FailOnIteration()):
+ response = self.client.get("/regular/basic/")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "djDebug") # toolbar
+
def test_should_render_panels_RENDER_PANELS(self):
"""
The toolbar should force rendering panels on each request
From 9c4eb6778899eb796cd97c92fae9fdcece34939e Mon Sep 17 00:00:00 2001
From: Matthias Kestenholz
Date: Sat, 1 Jun 2024 13:50:28 +0200
Subject: [PATCH 03/12] Amend the changelog
---
docs/changes.rst | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/changes.rst b/docs/changes.rst
index 20272fda7..2eaece240 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,7 +4,8 @@ Change log
Pending
-------
-* Fix overriding font-family for both light and dark themes.
+* Fixed overriding font-family for both light and dark themes.
+* Restored compatibility with ``iptools.IpRangeList``.
4.4.2 (2024-05-27)
------------------
From b5cf78460d8a36d83ff8aadb97f01a6346527632 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 10 Jun 2024 19:53:44 +0200
Subject: [PATCH 04/12] [pre-commit.ci] pre-commit autoupdate (#1931)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/adamchainz/django-upgrade: 1.17.0 → 1.18.0](https://github.com/adamchainz/django-upgrade/compare/1.17.0...1.18.0)
- [github.com/pre-commit/mirrors-eslint: v9.3.0 → v9.4.0](https://github.com/pre-commit/mirrors-eslint/compare/v9.3.0...v9.4.0)
- [github.com/astral-sh/ruff-pre-commit: v0.4.5 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.5...v0.4.8)
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 af8ab8b6f..85d167b43 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.17.0
+ rev: 1.18.0
hooks:
- id: django-upgrade
args: [--target-version, "4.2"]
@@ -32,7 +32,7 @@ repos:
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v9.3.0
+ rev: v9.4.0
hooks:
- id: eslint
additional_dependencies:
@@ -44,7 +44,7 @@ repos:
args:
- --fix
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 'v0.4.5'
+ rev: 'v0.4.8'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
From 0a622dcab4c78ce0f1d339f0e3147a1023350efa Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 17 Jun 2024 19:43:35 +0200
Subject: [PATCH 05/12] [pre-commit.ci] pre-commit autoupdate (#1934)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/mirrors-eslint: v9.4.0 → v9.5.0](https://github.com/pre-commit/mirrors-eslint/compare/v9.4.0...v9.5.0)
- [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.8...v0.4.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 85d167b43..04399e7cc 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.4.0
+ rev: v9.5.0
hooks:
- id: eslint
additional_dependencies:
@@ -44,7 +44,7 @@ repos:
args:
- --fix
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 'v0.4.8'
+ rev: 'v0.4.9'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
From 2b0790527a20354123b0f7fa093a0ec7a41bbe5a Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 24 Jun 2024 20:01:00 +0200
Subject: [PATCH 06/12] [pre-commit.ci] pre-commit autoupdate (#1939)
---
.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 04399e7cc..62d50b2d8 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.4.9'
+ rev: 'v0.4.10'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
From c54f898c0900c1bd94c29f9896ed97730ba812f7 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 20:04:17 +0200
Subject: [PATCH 07/12] [pre-commit.ci] pre-commit autoupdate (#1940)
---
.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 62d50b2d8..54a49e4d6 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.18.0
+ rev: 1.19.0
hooks:
- id: django-upgrade
args: [--target-version, "4.2"]
@@ -32,7 +32,7 @@ repos:
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v9.5.0
+ rev: v9.6.0
hooks:
- id: eslint
additional_dependencies:
@@ -44,7 +44,7 @@ repos:
args:
- --fix
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 'v0.4.10'
+ rev: 'v0.5.0'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
From 2d9c6a7cdd448429047623ab4985c9ef984b9735 Mon Sep 17 00:00:00 2001
From: Tim Schilling
Date: Wed, 3 Jul 2024 11:06:58 -0500
Subject: [PATCH 08/12] Limit the cases for E001 to likely scenarios (#1925)
Only when the user is customizing both the show toolbar callback
setting and the URLs aren't installed will the underlying
NoReverseMatch error occur.
---
debug_toolbar/apps.py | 32 +++++++++++++-
docs/changes.rst | 3 ++
docs/configuration.rst | 10 +++++
tests/test_checks.py | 99 +++++++++++++++++++++++++++---------------
4 files changed, 108 insertions(+), 36 deletions(-)
diff --git a/debug_toolbar/apps.py b/debug_toolbar/apps.py
index a2e977d84..a49875bac 100644
--- a/debug_toolbar/apps.py
+++ b/debug_toolbar/apps.py
@@ -5,10 +5,12 @@
from django.conf import settings
from django.core.checks import Error, Warning, register
from django.middleware.gzip import GZipMiddleware
+from django.urls import NoReverseMatch, reverse
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
-from debug_toolbar import settings as dt_settings
+from debug_toolbar import APP_NAME, settings as dt_settings
+from debug_toolbar.settings import CONFIG_DEFAULTS
class DebugToolbarConfig(AppConfig):
@@ -213,7 +215,33 @@ 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"]:
+ # Check if show toolbar callback has changed
+ show_toolbar_changed = (
+ dt_settings.get_config()["SHOW_TOOLBAR_CALLBACK"]
+ != CONFIG_DEFAULTS["SHOW_TOOLBAR_CALLBACK"]
+ )
+ try:
+ # Check if the toolbar's urls are installed
+ reverse(f"{APP_NAME}:render_panel")
+ toolbar_urls_installed = True
+ except NoReverseMatch:
+ toolbar_urls_installed = False
+
+ # If the user is using the default SHOW_TOOLBAR_CALLBACK,
+ # then the middleware will respect the change to settings.DEBUG.
+ # However, if the user has changed the callback to:
+ # DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG}
+ # where DEBUG is not settings.DEBUG, then it won't pick up that Django'
+ # test runner has changed the value for settings.DEBUG, and the middleware
+ # will inject the toolbar, while the URLs aren't configured leading to a
+ # NoReverseMatch error.
+ likely_error_setup = show_toolbar_changed and not toolbar_urls_installed
+
+ if (
+ not settings.DEBUG
+ and dt_settings.get_config()["IS_RUNNING_TESTS"]
+ and likely_error_setup
+ ):
return [
Error(
"The Django Debug Toolbar can't be used with tests",
diff --git a/docs/changes.rst b/docs/changes.rst
index 2eaece240..6dbfe829a 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -6,6 +6,9 @@ Pending
* Fixed overriding font-family for both light and dark themes.
* Restored compatibility with ``iptools.IpRangeList``.
+* Limit ``E001`` check to likely error cases when the
+ ``SHOW_TOOLBAR_CALLBACK`` has changed, but the toolbar's URL
+ paths aren't installed.
4.4.2 (2024-05-27)
------------------
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 04694aceb..80e44984f 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -152,6 +152,16 @@ Toolbar options
implication is that it is possible to execute arbitrary SQL through the
SQL panel when the ``SECRET_KEY`` value is leaked somehow.
+ .. warning::
+
+ Do not use
+ ``DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG}``
+ in your project's settings.py file. The toolbar expects to use
+ ``django.conf.settings.DEBUG``. Using your project's setting's ``DEBUG``
+ is likely to cause unexpected results when running your tests. This is because
+ Django automatically sets ``settings.DEBUG = False``, but your project's
+ setting's ``DEBUG`` will still be set to ``True``.
+
.. _OBSERVE_REQUEST_CALLBACK:
* ``OBSERVE_REQUEST_CALLBACK``
diff --git a/tests/test_checks.py b/tests/test_checks.py
index 827886db1..27db92a9d 100644
--- a/tests/test_checks.py
+++ b/tests/test_checks.py
@@ -1,9 +1,9 @@
from unittest.mock import patch
-from django.core.checks import Error, Warning, run_checks
+from django.core.checks import Warning, run_checks
from django.test import SimpleTestCase, override_settings
+from django.urls import NoReverseMatch
-from debug_toolbar import settings as dt_settings
from debug_toolbar.apps import debug_toolbar_installed_when_running_tests_check
@@ -239,39 +239,70 @@ 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)
+ @patch("debug_toolbar.apps.reverse")
+ def test_debug_toolbar_installed_when_running_tests(self, reverse):
+ params = [
+ {
+ "debug": True,
+ "running_tests": True,
+ "show_callback_changed": True,
+ "urls_installed": False,
+ "errors": False,
+ },
+ {
+ "debug": False,
+ "running_tests": False,
+ "show_callback_changed": True,
+ "urls_installed": False,
+ "errors": False,
+ },
+ {
+ "debug": False,
+ "running_tests": True,
+ "show_callback_changed": False,
+ "urls_installed": False,
+ "errors": False,
+ },
+ {
+ "debug": False,
+ "running_tests": True,
+ "show_callback_changed": True,
+ "urls_installed": True,
+ "errors": False,
+ },
+ {
+ "debug": False,
+ "running_tests": True,
+ "show_callback_changed": True,
+ "urls_installed": False,
+ "errors": True,
+ },
+ ]
+ for config in params:
+ with self.subTest(**config):
+ config_setting = {
+ "RENDER_PANELS": False,
+ "IS_RUNNING_TESTS": config["running_tests"],
+ "SHOW_TOOLBAR_CALLBACK": (
+ (lambda *args: True)
+ if config["show_callback_changed"]
+ else "debug_toolbar.middleware.show_toolbar"
+ ),
+ }
+ if config["urls_installed"]:
+ reverse.side_effect = lambda *args: None
+ else:
+ reverse.side_effect = NoReverseMatch()
- 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",
- )
- ],
- )
+ with self.settings(
+ DEBUG=config["debug"], DEBUG_TOOLBAR_CONFIG=config_setting
+ ):
+ errors = debug_toolbar_installed_when_running_tests_check(None)
+ if config["errors"]:
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].id, "debug_toolbar.E001")
+ else:
+ self.assertEqual(len(errors), 0)
@override_settings(
DEBUG_TOOLBAR_CONFIG={
From 325ea1940dc178ac05718504be01e07250298de3 Mon Sep 17 00:00:00 2001
From: Tim Schilling
Date: Wed, 3 Jul 2024 11:14:13 -0500
Subject: [PATCH 09/12] Introduce debug_toolbar_urls to simplify installation
(#1926)
While this isn't a huge improvement, it provides a hook for us
to more easily introduce library level controls for when urls
are installed. It also eliminates the user from having to
write an if statement for the most basic of installations.
---
debug_toolbar/toolbar.py | 28 +++++++++++++++++++++++++++-
docs/changes.rst | 2 ++
docs/installation.rst | 16 +++++++++-------
example/urls.py | 11 +++--------
tests/test_toolbar.py | 17 +++++++++++++++++
5 files changed, 58 insertions(+), 16 deletions(-)
create mode 100644 tests/test_toolbar.py
diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py
index fc07543b5..04502ab09 100644
--- a/debug_toolbar/toolbar.py
+++ b/debug_toolbar/toolbar.py
@@ -2,16 +2,18 @@
The main DebugToolbar class that loads and renders the Toolbar.
"""
+import re
import uuid
from collections import OrderedDict
from functools import lru_cache
from django.apps import apps
+from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.dispatch import Signal
from django.template import TemplateSyntaxError
from django.template.loader import render_to_string
-from django.urls import path, resolve
+from django.urls import include, path, re_path, resolve
from django.urls.exceptions import Resolver404
from django.utils.module_loading import import_string
from django.utils.translation import get_language, override as lang_override
@@ -186,3 +188,27 @@ def observe_request(request):
Determine whether to update the toolbar from a client side request.
"""
return True
+
+
+def debug_toolbar_urls(prefix="__debug__"):
+ """
+ Return a URL pattern for serving toolbar in debug mode.
+
+ from django.conf import settings
+ from debug_toolbar.toolbar import debug_toolbar_urls
+
+ urlpatterns = [
+ # ... the rest of your URLconf goes here ...
+ ] + debug_toolbar_urls()
+ """
+ if not prefix:
+ raise ImproperlyConfigured("Empty urls prefix not permitted")
+ elif not settings.DEBUG:
+ # No-op if not in debug mode.
+ return []
+ return [
+ re_path(
+ r"^%s/" % re.escape(prefix.lstrip("/")),
+ include("debug_toolbar.urls"),
+ ),
+ ]
diff --git a/docs/changes.rst b/docs/changes.rst
index 6dbfe829a..10883b1e2 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -9,6 +9,8 @@ Pending
* Limit ``E001`` check to likely error cases when the
``SHOW_TOOLBAR_CALLBACK`` has changed, but the toolbar's URL
paths aren't installed.
+* Introduce helper function ``debug_toolbar_urls`` to
+ simplify installation.
4.4.2 (2024-05-27)
------------------
diff --git a/docs/installation.rst b/docs/installation.rst
index 657450fac..9200504b7 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -95,14 +95,14 @@ Add django-debug-toolbar's URLs to your project's URLconf:
.. code-block:: python
from django.urls import include, path
+ from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
- # ...
- path("__debug__/", include("debug_toolbar.urls")),
- ]
+ # ... the rest of your URLconf goes here ...
+ ] + debug_toolbar_urls()
-This example uses the ``__debug__`` prefix, but you can use any prefix that
-doesn't clash with your application's URLs.
+By default this uses the ``__debug__`` prefix for the paths, but you can
+use any prefix that doesn't clash with your application's URLs.
5. Add the Middleware
@@ -180,11 +180,13 @@ You should also modify your URLconf file:
.. code-block:: python
+ from django.conf import settings
+ from debug_toolbar.toolbar import debug_toolbar_urls
+
if not settings.TESTING:
urlpatterns = [
*urlpatterns,
- path("__debug__/", include("debug_toolbar.urls")),
- ]
+ ] + debug_toolbar_urls()
Alternatively, you can check out the :ref:`IS_RUNNING_TESTS `
option.
diff --git a/example/urls.py b/example/urls.py
index 7569a57f9..b64aa1c37 100644
--- a/example/urls.py
+++ b/example/urls.py
@@ -1,8 +1,8 @@
-from django.conf import settings
from django.contrib import admin
-from django.urls import include, path
+from django.urls import path
from django.views.generic import TemplateView
+from debug_toolbar.toolbar import debug_toolbar_urls
from example.views import increment
urlpatterns = [
@@ -34,9 +34,4 @@
),
path("admin/", admin.site.urls),
path("ajax/increment", increment, name="ajax_increment"),
-]
-
-if settings.ENABLE_DEBUG_TOOLBAR:
- urlpatterns += [
- path("__debug__/", include("debug_toolbar.urls")),
- ]
+] + debug_toolbar_urls()
diff --git a/tests/test_toolbar.py b/tests/test_toolbar.py
new file mode 100644
index 000000000..7155d3fcb
--- /dev/null
+++ b/tests/test_toolbar.py
@@ -0,0 +1,17 @@
+from django.core.exceptions import ImproperlyConfigured
+
+from debug_toolbar.toolbar import debug_toolbar_urls
+from tests.base import BaseTestCase
+
+
+class DebugToolbarUrlsTestCase(BaseTestCase):
+ def test_empty_prefix_errors(self):
+ with self.assertRaises(ImproperlyConfigured):
+ debug_toolbar_urls(prefix="")
+
+ def test_empty_when_debug_is_false(self):
+ self.assertEqual(debug_toolbar_urls(), [])
+
+ def test_has_path(self):
+ with self.settings(DEBUG=True):
+ self.assertEqual(len(debug_toolbar_urls()), 1)
From d0f09b3a6bd86414d94bb397bf2e5dfe0f4db376 Mon Sep 17 00:00:00 2001
From: "Bart K. de Koning" <118313986+bkdekoning@users.noreply.github.com>
Date: Thu, 4 Jul 2024 12:18:41 -0400
Subject: [PATCH 10/12] Fixed #1682 -- alert user when using file field without
proper encoding (#1933)
* Fixed issue #1682 -- alert user when using file field without proper encoding
* Changed issues to alerts, added docstring to check_invalid_... function, changed call in nav_subtitle to get_stats
* Update AlertsPanel documentation to list pre-defined alert cases
* added check for file inputs that directly reference a form, including tests; also added tests for setting encoding in submit input type
* Update the alert messages to be on the panel as a map.
This also explicitly mentions what attribute the form needs in
the message.
* Expose a page in the example app that triggers the alerts panel.
---------
Co-authored-by: Tim Schilling
---
debug_toolbar/panels/alerts.py | 148 ++++++++++++++++++
debug_toolbar/settings.py | 1 +
.../debug_toolbar/panels/alerts.html | 12 ++
docs/changes.rst | 2 +
docs/configuration.rst | 1 +
docs/panels.rst | 11 ++
example/templates/bad_form.html | 14 ++
example/templates/index.html | 1 +
example/urls.py | 5 +
tests/panels/test_alerts.py | 101 ++++++++++++
tests/panels/test_history.py | 1 +
11 files changed, 297 insertions(+)
create mode 100644 debug_toolbar/panels/alerts.py
create mode 100644 debug_toolbar/templates/debug_toolbar/panels/alerts.html
create mode 100644 example/templates/bad_form.html
create mode 100644 tests/panels/test_alerts.py
diff --git a/debug_toolbar/panels/alerts.py b/debug_toolbar/panels/alerts.py
new file mode 100644
index 000000000..27a7119ee
--- /dev/null
+++ b/debug_toolbar/panels/alerts.py
@@ -0,0 +1,148 @@
+from html.parser import HTMLParser
+
+from django.utils.translation import gettext_lazy as _
+
+from debug_toolbar.panels import Panel
+
+
+class FormParser(HTMLParser):
+ """
+ HTML form parser, used to check for invalid configurations of forms that
+ take file inputs.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.in_form = False
+ self.current_form = {}
+ self.forms = []
+ self.form_ids = []
+ self.referenced_file_inputs = []
+
+ def handle_starttag(self, tag, attrs):
+ attrs = dict(attrs)
+ if tag == "form":
+ self.in_form = True
+ form_id = attrs.get("id")
+ if form_id:
+ self.form_ids.append(form_id)
+ self.current_form = {
+ "file_form": False,
+ "form_attrs": attrs,
+ "submit_element_attrs": [],
+ }
+ elif (
+ self.in_form
+ and tag == "input"
+ and attrs.get("type") == "file"
+ and (not attrs.get("form") or attrs.get("form") == "")
+ ):
+ self.current_form["file_form"] = True
+ elif (
+ self.in_form
+ and (
+ (tag == "input" and attrs.get("type") in {"submit", "image"})
+ or tag == "button"
+ )
+ and (not attrs.get("form") or attrs.get("form") == "")
+ ):
+ self.current_form["submit_element_attrs"].append(attrs)
+ elif tag == "input" and attrs.get("form"):
+ self.referenced_file_inputs.append(attrs)
+
+ def handle_endtag(self, tag):
+ if tag == "form" and self.in_form:
+ self.forms.append(self.current_form)
+ self.in_form = False
+
+
+class AlertsPanel(Panel):
+ """
+ A panel to alert users to issues.
+ """
+
+ messages = {
+ "form_id_missing_enctype": _(
+ 'Form with id "{form_id}" contains file input, but does not have the attribute enctype="multipart/form-data".'
+ ),
+ "form_missing_enctype": _(
+ 'Form contains file input, but does not have the attribute enctype="multipart/form-data".'
+ ),
+ "input_refs_form_missing_enctype": _(
+ 'Input element references form with id "{form_id}", but the form does not have the attribute enctype="multipart/form-data".'
+ ),
+ }
+
+ title = _("Alerts")
+
+ template = "debug_toolbar/panels/alerts.html"
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.alerts = []
+
+ @property
+ def nav_subtitle(self):
+ alerts = self.get_stats()["alerts"]
+ if alerts:
+ alert_text = "alert" if len(alerts) == 1 else "alerts"
+ return f"{len(alerts)} {alert_text}"
+ else:
+ return ""
+
+ def add_alert(self, alert):
+ self.alerts.append(alert)
+
+ def check_invalid_file_form_configuration(self, html_content):
+ """
+ Inspects HTML content for a form that includes a file input but does
+ not have the encoding type set to multipart/form-data, and warns the
+ user if so.
+ """
+ parser = FormParser()
+ parser.feed(html_content)
+
+ # Check for file inputs directly inside a form that do not reference
+ # any form through the form attribute
+ for form in parser.forms:
+ if (
+ form["file_form"]
+ and form["form_attrs"].get("enctype") != "multipart/form-data"
+ and not any(
+ elem.get("formenctype") == "multipart/form-data"
+ for elem in form["submit_element_attrs"]
+ )
+ ):
+ if form_id := form["form_attrs"].get("id"):
+ alert = self.messages["form_id_missing_enctype"].format(
+ form_id=form_id
+ )
+ else:
+ alert = self.messages["form_missing_enctype"]
+ self.add_alert({"alert": alert})
+
+ # Check for file inputs that reference a form
+ form_attrs_by_id = {
+ form["form_attrs"].get("id"): form["form_attrs"] for form in parser.forms
+ }
+
+ for attrs in parser.referenced_file_inputs:
+ form_id = attrs.get("form")
+ if form_id and attrs.get("type") == "file":
+ form_attrs = form_attrs_by_id.get(form_id)
+ if form_attrs and form_attrs.get("enctype") != "multipart/form-data":
+ alert = self.messages["input_refs_form_missing_enctype"].format(
+ form_id=form_id
+ )
+ self.add_alert({"alert": alert})
+
+ return self.alerts
+
+ def generate_stats(self, request, response):
+ html_content = response.content.decode(response.charset)
+ self.check_invalid_file_form_configuration(html_content)
+
+ # Further alert checks can go here
+
+ # Write all alerts to record_stats
+ self.record_stats({"alerts": self.alerts})
diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py
index ca7036c34..48483cf40 100644
--- a/debug_toolbar/settings.py
+++ b/debug_toolbar/settings.py
@@ -67,6 +67,7 @@ def get_config():
"debug_toolbar.panels.sql.SQLPanel",
"debug_toolbar.panels.staticfiles.StaticFilesPanel",
"debug_toolbar.panels.templates.TemplatesPanel",
+ "debug_toolbar.panels.alerts.AlertsPanel",
"debug_toolbar.panels.cache.CachePanel",
"debug_toolbar.panels.signals.SignalsPanel",
"debug_toolbar.panels.redirects.RedirectsPanel",
diff --git a/debug_toolbar/templates/debug_toolbar/panels/alerts.html b/debug_toolbar/templates/debug_toolbar/panels/alerts.html
new file mode 100644
index 000000000..df208836d
--- /dev/null
+++ b/debug_toolbar/templates/debug_toolbar/panels/alerts.html
@@ -0,0 +1,12 @@
+{% load i18n %}
+
+{% if alerts %}
+
{% trans "Alerts found" %}
+ {% for alert in alerts %}
+
+
{{ alert.alert }}
+
+ {% endfor %}
+{% else %}
+
{% trans "No alerts found" %}
+{% endif %}
diff --git a/docs/changes.rst b/docs/changes.rst
index 10883b1e2..fd642e980 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,6 +4,8 @@ Change log
Pending
-------
+* Added alert panel with warning when form is using file fields
+ without proper encoding type.
* Fixed overriding font-family for both light and dark themes.
* Restored compatibility with ``iptools.IpRangeList``.
* Limit ``E001`` check to likely error cases when the
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 80e44984f..e4ccc1dae 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -29,6 +29,7 @@ default value is::
'debug_toolbar.panels.sql.SQLPanel',
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
'debug_toolbar.panels.templates.TemplatesPanel',
+ 'debug_toolbar.panels.alerts.AlertsPanel',
'debug_toolbar.panels.cache.CachePanel',
'debug_toolbar.panels.signals.SignalsPanel',
'debug_toolbar.panels.redirects.RedirectsPanel',
diff --git a/docs/panels.rst b/docs/panels.rst
index 33359ea46..c9ea6bbf0 100644
--- a/docs/panels.rst
+++ b/docs/panels.rst
@@ -9,6 +9,17 @@ Default built-in panels
The following panels are enabled by default.
+Alerts
+~~~~~~~
+
+.. class:: debug_toolbar.panels.alerts.AlertsPanel
+
+This panel shows alerts for a set of pre-defined cases:
+
+- Alerts when the response has a form without the
+ ``enctype="multipart/form-data"`` attribute and the form contains
+ a file input.
+
History
~~~~~~~
diff --git a/example/templates/bad_form.html b/example/templates/bad_form.html
new file mode 100644
index 000000000..f50662c6e
--- /dev/null
+++ b/example/templates/bad_form.html
@@ -0,0 +1,14 @@
+{% load cache %}
+
+
+
+
+ Bad form
+
+
+