├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── coverage.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .tx └── config ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.rst ├── SECURITY.md ├── biome.json ├── debug_toolbar ├── __init__.py ├── _compat.py ├── _stubs.py ├── apps.py ├── decorators.py ├── forms.py ├── locale │ ├── bg │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ca │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── cs │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fi │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── he │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── id │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ja │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ko │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── sk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── sv_SE │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── uk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_CN │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── debugsqlshell.py ├── middleware.py ├── panels │ ├── __init__.py │ ├── alerts.py │ ├── cache.py │ ├── headers.py │ ├── history │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── panel.py │ │ └── views.py │ ├── profiling.py │ ├── redirects.py │ ├── request.py │ ├── settings.py │ ├── signals.py │ ├── sql │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── panel.py │ │ ├── tracking.py │ │ ├── utils.py │ │ └── views.py │ ├── staticfiles.py │ ├── templates │ │ ├── __init__.py │ │ ├── jinja2.py │ │ ├── panel.py │ │ └── views.py │ ├── timer.py │ └── versions.py ├── settings.py ├── static │ └── debug_toolbar │ │ ├── css │ │ ├── print.css │ │ └── toolbar.css │ │ └── js │ │ ├── history.js │ │ ├── redirect.js │ │ ├── timer.js │ │ ├── toolbar.js │ │ └── utils.js ├── templates │ └── debug_toolbar │ │ ├── base.html │ │ ├── includes │ │ ├── panel_button.html │ │ ├── panel_content.html │ │ └── theme_selector.html │ │ ├── panels │ │ ├── alerts.html │ │ ├── cache.html │ │ ├── headers.html │ │ ├── history.html │ │ ├── history_tr.html │ │ ├── profiling.html │ │ ├── request.html │ │ ├── request_variables.html │ │ ├── settings.html │ │ ├── signals.html │ │ ├── sql.html │ │ ├── sql_explain.html │ │ ├── sql_profile.html │ │ ├── sql_select.html │ │ ├── staticfiles.html │ │ ├── template_source.html │ │ ├── templates.html │ │ ├── timer.html │ │ └── versions.html │ │ └── redirect.html ├── templatetags │ └── __init__.py ├── toolbar.py ├── urls.py ├── utils.py └── views.py ├── docs ├── Makefile ├── architecture.rst ├── changes.rst ├── checks.rst ├── commands.rst ├── conf.py ├── configuration.rst ├── contributing.rst ├── index.rst ├── installation.rst ├── make.bat ├── panels.rst ├── resources.rst ├── spelling_wordlist.txt └── tips.rst ├── example ├── README.rst ├── __init__.py ├── asgi.py ├── django-debug-toolbar.png ├── example.db ├── manage.py ├── screenshot.py ├── settings.py ├── static │ └── test.css ├── templates │ ├── async_db.html │ ├── bad_form.html │ ├── htmx │ │ └── boost.html │ ├── index.html │ ├── jinja2 │ │ └── index.jinja │ ├── jquery │ │ └── index.html │ ├── mootools │ │ └── index.html │ ├── prototype │ │ └── index.html │ └── turbo │ │ └── index.html ├── test_views.py ├── urls.py ├── views.py └── wsgi.py ├── pyproject.toml ├── requirements_dev.txt ├── setup.py ├── tests ├── __init__.py ├── additional_static │ └── base.css ├── base.py ├── commands │ ├── __init__.py │ └── test_debugsqlshell.py ├── context_processors.py ├── forms.py ├── loaders.py ├── middleware.py ├── models.py ├── panels │ ├── __init__.py │ ├── test_alerts.py │ ├── test_async_panel_compatibility.py │ ├── test_cache.py │ ├── test_custom.py │ ├── test_history.py │ ├── test_profiling.py │ ├── test_redirects.py │ ├── test_request.py │ ├── test_settings.py │ ├── test_sql.py │ ├── test_staticfiles.py │ ├── test_template.py │ └── test_versions.py ├── settings.py ├── sync.py ├── templates │ ├── ajax │ │ └── ajax.html │ ├── base.html │ ├── basic.html │ ├── jinja2 │ │ ├── base.html │ │ └── basic.jinja │ ├── registration │ │ └── login.html │ ├── sql │ │ ├── flat.html │ │ ├── included.html │ │ └── nested.html │ └── staticfiles │ │ ├── async_static.html │ │ └── path.html ├── test_checks.py ├── test_csp_rendering.py ├── test_decorators.py ├── test_forms.py ├── test_integration.py ├── test_integration_async.py ├── test_login_not_required.py ├── test_middleware.py ├── test_settings.py ├── test_toolbar.py ├── test_utils.py ├── urls.py ├── urls_invalid.py ├── urls_use_package_urls.py └── views.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | [*.html] 14 | indent_size = 2 15 | 16 | [Makefile] 17 | indent_style = tab 18 | 19 | [*.bat] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also 4 | include relevant motivation and context. Your commit message should include 5 | this information as well. 6 | 7 | Fixes # (issue) 8 | 9 | #### Checklist: 10 | 11 | - [ ] I have added the relevant tests for this change. 12 | - [ ] I have added an item to the Pending section of ``docs/changes.rst``. 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/coverage.yml 2 | name: Post coverage comment 3 | 4 | on: 5 | workflow_run: 6 | workflows: ["Test"] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | test: 12 | name: Run tests & display coverage 13 | runs-on: ubuntu-latest 14 | if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' 15 | permissions: 16 | # Gives the action the necessary permissions for publishing new 17 | # comments in pull requests. 18 | pull-requests: write 19 | # Gives the action the necessary permissions for editing existing 20 | # comments (to avoid publishing multiple comments in the same PR) 21 | contents: write 22 | # Gives the action the necessary permissions for looking up the 23 | # workflow that launched this workflow, and download the related 24 | # artifact that contains the comment to be published 25 | actions: read 26 | steps: 27 | # DO NOT run actions/checkout here, for security reasons 28 | # For details, refer to https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 29 | - name: Post comment 30 | uses: py-cov-action/python-coverage-comment-action@v3 31 | with: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }} 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI 2 | 3 | on: push 4 | 5 | env: 6 | PYPI_URL: https://pypi.org/p/django-debug-toolbar 7 | PYPI_TEST_URL: https://test.pypi.org/p/django-debug-toolbar 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build distribution 📦 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.x" 21 | - name: Install pypa/build 22 | run: 23 | python3 -m pip install build --user 24 | - name: Build a binary wheel and a source tarball 25 | run: python3 -m build 26 | - name: Store the distribution packages 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: python-package-distributions 30 | path: dist/ 31 | 32 | publish-to-pypi: 33 | name: >- 34 | Publish Python 🐍 distribution 📦 to PyPI 35 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 36 | needs: 37 | - build 38 | runs-on: ubuntu-latest 39 | environment: 40 | name: pypi 41 | url: ${{ env.PYPI_URL }} 42 | permissions: 43 | id-token: write # IMPORTANT: mandatory for trusted publishing 44 | steps: 45 | - name: Download all the dists 46 | uses: actions/download-artifact@v4 47 | with: 48 | name: python-package-distributions 49 | path: dist/ 50 | - name: Publish distribution 📦 to PyPI 51 | uses: pypa/gh-action-pypi-publish@release/v1.12 52 | 53 | github-release: 54 | name: >- 55 | Sign the Python 🐍 distribution 📦 with Sigstore 56 | and upload them to GitHub Release 57 | needs: 58 | - publish-to-pypi 59 | runs-on: ubuntu-latest 60 | 61 | permissions: 62 | contents: write # IMPORTANT: mandatory for making GitHub Releases 63 | id-token: write # IMPORTANT: mandatory for sigstore 64 | 65 | steps: 66 | - name: Download all the dists 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: python-package-distributions 70 | path: dist/ 71 | - name: Sign the dists with Sigstore 72 | uses: sigstore/gh-action-sigstore-python@v3.0.0 73 | with: 74 | inputs: >- 75 | ./dist/*.tar.gz 76 | ./dist/*.whl 77 | - name: Create GitHub Release 78 | env: 79 | GITHUB_TOKEN: ${{ github.token }} 80 | run: >- 81 | gh release create 82 | '${{ github.ref_name }}' 83 | --repo '${{ github.repository }}' 84 | --notes "" 85 | - name: Upload artifact signatures to GitHub Release 86 | env: 87 | GITHUB_TOKEN: ${{ github.token }} 88 | # Upload to GitHub Release using the `gh` CLI. 89 | # `dist/` contains the built packages, and the 90 | # sigstore-produced signatures and certificates. 91 | run: >- 92 | gh release upload 93 | '${{ github.ref_name }}' dist/** 94 | --repo '${{ github.repository }}' 95 | 96 | publish-to-testpypi: 97 | name: Publish Python 🐍 distribution 📦 to TestPyPI 98 | if: startsWith(github.ref, 'refs/tags/') # only publish to Test PyPI on tag pushes 99 | needs: 100 | - build 101 | runs-on: ubuntu-latest 102 | 103 | environment: 104 | name: testpypi 105 | url: ${{ env.PYPI_TEST_URL }} 106 | 107 | permissions: 108 | id-token: write # IMPORTANT: mandatory for trusted publishing 109 | 110 | steps: 111 | - name: Download all the dists 112 | uses: actions/download-artifact@v4 113 | with: 114 | name: python-package-distributions 115 | path: dist/ 116 | - name: Publish distribution 📦 to TestPyPI 117 | uses: pypa/gh-action-pypi-publish@release/v1.12 118 | with: 119 | repository-url: https://test.pypi.org/legacy/ 120 | skip-existing: true 121 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DS_Store 3 | *~ 4 | .idea 5 | build 6 | .coverage* 7 | dist 8 | django_debug_toolbar.egg-info 9 | docs/_build 10 | example/db.sqlite3 11 | htmlcov 12 | .tox 13 | geckodriver.log 14 | coverage.xml 15 | .direnv/ 16 | .envrc 17 | venv 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-toml 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: mixed-line-ending 10 | - id: file-contents-sorter 11 | files: docs/spelling_wordlist.txt 12 | - repo: https://github.com/pycqa/doc8 13 | rev: v1.1.2 14 | hooks: 15 | - id: doc8 16 | - repo: https://github.com/adamchainz/django-upgrade 17 | rev: 1.25.0 18 | hooks: 19 | - id: django-upgrade 20 | args: [--target-version, "4.2"] 21 | - repo: https://github.com/adamchainz/djade-pre-commit 22 | rev: "1.4.0" 23 | hooks: 24 | - id: djade 25 | args: [--target-version, "4.2"] 26 | - repo: https://github.com/pre-commit/pygrep-hooks 27 | rev: v1.10.0 28 | hooks: 29 | - id: rst-backticks 30 | - id: rst-directive-colons 31 | - repo: https://github.com/biomejs/pre-commit 32 | rev: v2.0.0-beta.5 33 | hooks: 34 | - id: biome-check 35 | verbose: true 36 | - repo: https://github.com/astral-sh/ruff-pre-commit 37 | rev: 'v0.11.12' 38 | hooks: 39 | - id: ruff 40 | args: [--fix, --exit-non-zero-on-fix] 41 | - id: ruff-format 42 | - repo: https://github.com/tox-dev/pyproject-fmt 43 | rev: v2.6.0 44 | hooks: 45 | - id: pyproject-fmt 46 | - repo: https://github.com/abravalheri/validate-pyproject 47 | rev: v0.24.1 48 | hooks: 49 | - id: validate-pyproject 50 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-24.04 9 | tools: 10 | python: "3.10" 11 | 12 | sphinx: 13 | configuration: docs/conf.py 14 | 15 | python: 16 | install: 17 | - requirements: requirements_dev.txt 18 | - method: pip 19 | path: . 20 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = sr@latin: sr_Latn 4 | 5 | [o:django-debug-toolbar:p:django-debug-toolbar:r:main] 6 | file_filter = debug_toolbar/locale//LC_MESSAGES/django.po 7 | source_file = debug_toolbar/locale/en/LC_MESSAGES/django.po 8 | source_lang = en 9 | replace_edited_strings = false 10 | keep_translations = false 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Django Debug Toolbar Code of Conduct 2 | 3 | The django-debug-toolbar project utilizes the [Django Commons Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Django Debug Toolbar 2 | 3 | This is a [Django Commons](https://github.com/django-commons/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md). 4 | 5 | ## Documentation 6 | 7 | For detailed contributing guidelines, please see our [Documentation](https://django-debug-toolbar.readthedocs.io/en/latest/contributing.html). 8 | 9 | ## Additional Resources 10 | 11 | Please see the [README](https://github.com/django-commons/membership/blob/main/README.md) for more help. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Rob Hudson and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: example test coverage translatable_strings update_translations help 2 | .DEFAULT_GOAL := help 3 | 4 | example: ## Run the example application 5 | python example/manage.py migrate --noinput 6 | -DJANGO_SUPERUSER_PASSWORD=p python example/manage.py createsuperuser \ 7 | --noinput --username="$(USER)" --email="$(USER)@mailinator.com" 8 | python example/manage.py runserver 9 | 10 | example_test: ## Run the test suite for the example application 11 | python example/manage.py test example 12 | 13 | test: ## Run the test suite 14 | DJANGO_SETTINGS_MODULE=tests.settings \ 15 | python -m django test $${TEST_ARGS:-tests} 16 | 17 | test_selenium: ## Run frontend tests written with Selenium 18 | DJANGO_SELENIUM_TESTS=true DJANGO_SETTINGS_MODULE=tests.settings \ 19 | python -m django test $${TEST_ARGS:-tests} 20 | 21 | coverage: ## Run the test suite with coverage enabled 22 | python --version 23 | DJANGO_SETTINGS_MODULE=tests.settings \ 24 | python -b -W always -m coverage run -m django test -v2 $${TEST_ARGS:-tests} 25 | coverage report 26 | coverage html 27 | coverage xml 28 | 29 | translatable_strings: ## Update the English '.po' file 30 | cd debug_toolbar && python -m django makemessages -l en --no-obsolete 31 | @echo "Please commit changes and run 'tx push -s' (or wait for Transifex to pick them)" 32 | 33 | update_translations: ## Download updated '.po' files from Transifex 34 | tx pull -a --minimum-perc=10 35 | cd debug_toolbar && python -m django compilemessages 36 | 37 | .PHONY: example/django-debug-toolbar.png 38 | example/django-debug-toolbar.png: example/screenshot.py ## Update the screenshot in 'README.rst' 39 | python $< --browser firefox --headless -o $@ 40 | optipng $@ 41 | 42 | help: ## Help message for targets 43 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ 44 | | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 45 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================================== 2 | Django Debug Toolbar |latest-version| 3 | ===================================== 4 | 5 | |build-status| |coverage| |docs| |python-support| |django-support| 6 | 7 | .. |latest-version| image:: https://img.shields.io/pypi/v/django-debug-toolbar.svg 8 | :target: https://pypi.org/project/django-debug-toolbar/ 9 | :alt: Latest version on PyPI 10 | 11 | .. |build-status| image:: https://github.com/django-commons/django-debug-toolbar/workflows/Test/badge.svg 12 | :target: https://github.com/django-commons/django-debug-toolbar/actions/workflows/test.yml 13 | :alt: Build Status 14 | 15 | .. |coverage| image:: https://img.shields.io/badge/Coverage-94%25-green 16 | :target: https://github.com/django-commons/django-debug-toolbar/actions/workflows/test.yml?query=branch%3Amain 17 | :alt: Test coverage status 18 | 19 | .. |docs| image:: https://img.shields.io/readthedocs/django-debug-toolbar/latest.svg 20 | :target: https://readthedocs.org/projects/django-debug-toolbar/ 21 | :alt: Documentation status 22 | 23 | .. |python-support| image:: https://img.shields.io/pypi/pyversions/django-debug-toolbar 24 | :target: https://pypi.org/project/django-debug-toolbar/ 25 | :alt: Supported Python versions 26 | 27 | .. |django-support| image:: https://img.shields.io/pypi/djversions/django-debug-toolbar 28 | :target: https://pypi.org/project/django-debug-toolbar/ 29 | :alt: Supported Django versions 30 | 31 | The Django Debug Toolbar is a configurable set of panels that display various 32 | debug information about the current request/response and when clicked, display 33 | more details about the panel's content. 34 | 35 | Here's a screenshot of the toolbar in action: 36 | 37 | .. image:: https://raw.github.com/django-commons/django-debug-toolbar/main/example/django-debug-toolbar.png 38 | :alt: Django Debug Toolbar screenshot 39 | 40 | In addition to the built-in panels, a number of third-party panels are 41 | contributed by the community. 42 | 43 | The current stable version of the Debug Toolbar is 5.2.0. It works on 44 | Django ≥ 4.2.0. 45 | 46 | The Debug Toolbar has experimental support for `Django's asynchronous views 47 | `_. Please note that 48 | the Debug Toolbar still lacks the capability for handling concurrent requests. 49 | If you find any issues, please report them on the `issue tracker`_. 50 | 51 | Documentation, including installation and configuration instructions, is 52 | available at https://django-debug-toolbar.readthedocs.io/. 53 | 54 | The Django Debug Toolbar is released under the BSD license, like Django 55 | itself. If you like it, please consider contributing! 56 | 57 | The Django Debug Toolbar was originally created by Rob Hudson 58 | in August 2008 and was further developed by many contributors_. 59 | 60 | .. _contributors: https://github.com/django-commons/django-debug-toolbar/graphs/contributors 61 | .. _issue tracker: https://github.com/django-commons/django-debug-toolbar/issues 62 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest version of django-debug-toolbar [![PyPI version](https://badge.fury.io/py/django-debug-toolbar.svg)](https://pypi.python.org/pypi/django-debug-toolbar) is supported. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you think you have found a vulnerability, and even if you are not sure, please [report it to us in private](https://github.com/django-commons/django-debug-toolbar/security/advisories/new). We will review it and get back to you. Please refrain from public discussions of the issue. 10 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.0.0-beta.5/schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "useEditorconfig": true 6 | }, 7 | "assist": { 8 | "actions": { 9 | "source": { 10 | "organizeImports": "on" 11 | } 12 | } 13 | }, 14 | "linter": { 15 | "enabled": true, 16 | "rules": { 17 | "recommended": true, 18 | "style": { 19 | "useLiteralEnumMembers": "error", 20 | "noCommaOperator": "error", 21 | "useNodejsImportProtocol": "error", 22 | "useAsConstAssertion": "error", 23 | "useEnumInitializers": "error", 24 | "useSelfClosingElements": "error", 25 | "useConst": "error", 26 | "useSingleVarDeclarator": "error", 27 | "noUnusedTemplateLiteral": "error", 28 | "useNumberNamespace": "error", 29 | "noInferrableTypes": "error", 30 | "useExponentiationOperator": "error", 31 | "useTemplate": "error", 32 | "noParameterAssign": "error", 33 | "noNonNullAssertion": "error", 34 | "useDefaultParameterLast": "error", 35 | "noArguments": "error", 36 | "useImportType": "error", 37 | "useExportType": "error", 38 | "noUselessElse": "error", 39 | "useShorthandFunctionType": "error" 40 | }, 41 | "suspicious": { 42 | "noDocumentCookie": "off" 43 | }, 44 | "complexity": { 45 | "useNumericLiterals": "error" 46 | } 47 | } 48 | }, 49 | "javascript": { 50 | "formatter": { 51 | "trailingCommas": "es5", 52 | "quoteStyle": "double" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /debug_toolbar/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["APP_NAME", "VERSION"] 2 | 3 | APP_NAME = "djdt" 4 | 5 | # Do not use pkg_resources to find the version but set it here directly! 6 | # see issue #1446 7 | VERSION = "5.2.0" 8 | 9 | # Code that discovers files or modules in INSTALLED_APPS imports this module. 10 | urls = "debug_toolbar.urls", APP_NAME 11 | -------------------------------------------------------------------------------- /debug_toolbar/_compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.contrib.auth.decorators import login_not_required 3 | except ImportError: 4 | # For Django < 5.1, copy the current Django implementation 5 | def login_not_required(view_func): 6 | """ 7 | Decorator for views that allows access to unauthenticated requests. 8 | """ 9 | view_func.login_required = False 10 | return view_func 11 | -------------------------------------------------------------------------------- /debug_toolbar/_stubs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, NamedTuple, Optional 4 | 5 | from django import template as dj_template 6 | 7 | 8 | class InspectStack(NamedTuple): 9 | frame: Any 10 | filename: str 11 | lineno: int 12 | function: str 13 | code_context: str 14 | index: int 15 | 16 | 17 | TidyStackTrace = list[tuple[str, int, str, str, Optional[Any]]] 18 | 19 | 20 | class RenderContext(dj_template.context.RenderContext): 21 | template: dj_template.Template 22 | 23 | 24 | class RequestContext(dj_template.RequestContext): 25 | template: dj_template.Template 26 | render_context: RenderContext 27 | -------------------------------------------------------------------------------- /debug_toolbar/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from asgiref.sync import iscoroutinefunction 4 | from django.http import Http404 5 | from django.utils.translation import get_language, override as language_override 6 | 7 | from debug_toolbar import settings as dt_settings 8 | 9 | 10 | def require_show_toolbar(view): 11 | """ 12 | Async compatible decorator to restrict access to a view 13 | based on the Debug Toolbar's visibility settings. 14 | """ 15 | from debug_toolbar.middleware import get_show_toolbar 16 | 17 | if iscoroutinefunction(view): 18 | 19 | @functools.wraps(view) 20 | async def inner(request, *args, **kwargs): 21 | show_toolbar = get_show_toolbar(async_mode=True) 22 | if not await show_toolbar(request): 23 | raise Http404 24 | 25 | return await view(request, *args, **kwargs) 26 | else: 27 | 28 | @functools.wraps(view) 29 | def inner(request, *args, **kwargs): 30 | show_toolbar = get_show_toolbar(async_mode=False) 31 | if not show_toolbar(request): 32 | raise Http404 33 | 34 | return view(request, *args, **kwargs) 35 | 36 | return inner 37 | 38 | 39 | def render_with_toolbar_language(view): 40 | """Force any rendering within the view to use the toolbar's language.""" 41 | 42 | @functools.wraps(view) 43 | def inner(request, *args, **kwargs): 44 | lang = dt_settings.get_config()["TOOLBAR_LANGUAGE"] or get_language() 45 | with language_override(lang): 46 | return view(request, *args, **kwargs) 47 | 48 | return inner 49 | -------------------------------------------------------------------------------- /debug_toolbar/forms.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import forms 4 | from django.core import signing 5 | from django.core.exceptions import ValidationError 6 | from django.utils.encoding import force_str 7 | 8 | 9 | class SignedDataForm(forms.Form): 10 | """Helper form that wraps a form to validate its contents on post. 11 | 12 | class PanelForm(forms.Form): 13 | # fields 14 | 15 | On render: 16 | form = SignedDataForm(initial=PanelForm(initial=data).initial) 17 | 18 | On POST: 19 | signed_form = SignedDataForm(request.POST) 20 | if signed_form.is_valid(): 21 | panel_form = PanelForm(signed_form.verified_data) 22 | if panel_form.is_valid(): 23 | # Success 24 | """ 25 | 26 | salt = "django_debug_toolbar" 27 | signed = forms.CharField(required=True, widget=forms.HiddenInput) 28 | 29 | def __init__(self, *args, **kwargs): 30 | initial = kwargs.pop("initial", None) 31 | if initial: 32 | initial = {"signed": self.sign(initial)} 33 | super().__init__(*args, initial=initial, **kwargs) 34 | 35 | def clean_signed(self): 36 | try: 37 | verified = json.loads( 38 | signing.Signer(salt=self.salt).unsign(self.cleaned_data["signed"]) 39 | ) 40 | return verified 41 | except signing.BadSignature as exc: 42 | raise ValidationError("Bad signature") from exc 43 | 44 | def verified_data(self): 45 | return self.is_valid() and self.cleaned_data["signed"] 46 | 47 | @classmethod 48 | def sign(cls, data): 49 | return signing.Signer(salt=cls.salt).sign( 50 | json.dumps({key: force_str(value) for key, value in data.items()}) 51 | ) 52 | -------------------------------------------------------------------------------- /debug_toolbar/locale/bg/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/bg/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/ca/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/ca/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/cs/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/cs/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/fi/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/fi/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/he/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/he/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/id/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/id/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/ja/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/ja/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/ko/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/ko/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/pt/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/pt/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/sk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/sk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/sv_SE/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/sv_SE/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/locale/zh_CN/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/locale/zh_CN/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /debug_toolbar/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/management/__init__.py -------------------------------------------------------------------------------- /debug_toolbar/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/management/commands/__init__.py -------------------------------------------------------------------------------- /debug_toolbar/management/commands/debugsqlshell.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | import sqlparse 4 | from django.core.management.commands.shell import Command 5 | from django.db import connection 6 | 7 | if connection.vendor == "postgresql": 8 | from django.db.backends.postgresql import base as base_module 9 | else: 10 | from django.db.backends import utils as base_module 11 | 12 | # 'debugsqlshell' is the same as the 'shell'. 13 | 14 | 15 | # Command is required to exist to be loaded via 16 | # django.core.managementload_command_class 17 | __all__ = ["Command", "PrintQueryWrapper"] 18 | 19 | 20 | class PrintQueryWrapper(base_module.CursorDebugWrapper): 21 | def execute(self, sql, params=()): 22 | start_time = perf_counter() 23 | try: 24 | return self.cursor.execute(sql, params) 25 | finally: 26 | raw_sql = self.db.ops.last_executed_query(self.cursor, sql, params) 27 | end_time = perf_counter() 28 | duration = (end_time - start_time) * 1000 29 | formatted_sql = sqlparse.format(raw_sql, reindent=True) 30 | print(f"{formatted_sql} [{duration:.2f}ms]") 31 | 32 | 33 | base_module.CursorDebugWrapper = PrintQueryWrapper 34 | -------------------------------------------------------------------------------- /debug_toolbar/panels/headers.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | 3 | from debug_toolbar.panels import Panel 4 | 5 | 6 | class HeadersPanel(Panel): 7 | """ 8 | A panel to display HTTP headers. 9 | """ 10 | 11 | # List of environment variables we want to display 12 | ENVIRON_FILTER = { 13 | "CONTENT_LENGTH", 14 | "CONTENT_TYPE", 15 | "DJANGO_SETTINGS_MODULE", 16 | "GATEWAY_INTERFACE", 17 | "QUERY_STRING", 18 | "PATH_INFO", 19 | "PYTHONPATH", 20 | "REMOTE_ADDR", 21 | "REMOTE_HOST", 22 | "REQUEST_METHOD", 23 | "SCRIPT_NAME", 24 | "SERVER_NAME", 25 | "SERVER_PORT", 26 | "SERVER_PROTOCOL", 27 | "SERVER_SOFTWARE", 28 | "TZ", 29 | } 30 | 31 | title = _("Headers") 32 | 33 | is_async = True 34 | 35 | template = "debug_toolbar/panels/headers.html" 36 | 37 | def process_request(self, request): 38 | wsgi_env = sorted(request.META.items()) 39 | self.request_headers = { 40 | unmangle(k): v for (k, v) in wsgi_env if is_http_header(k) 41 | } 42 | if "Cookie" in self.request_headers: 43 | self.request_headers["Cookie"] = "=> see Request panel" 44 | self.environ = {k: v for (k, v) in wsgi_env if k in self.ENVIRON_FILTER} 45 | self.record_stats( 46 | {"request_headers": self.request_headers, "environ": self.environ} 47 | ) 48 | return super().process_request(request) 49 | 50 | def generate_stats(self, request, response): 51 | self.response_headers = dict(sorted(response.items())) 52 | self.record_stats({"response_headers": self.response_headers}) 53 | 54 | 55 | def is_http_header(wsgi_key): 56 | # The WSGI spec says that keys should be str objects in the environ dict, 57 | # but this isn't true in practice. See issues #449 and #482. 58 | return isinstance(wsgi_key, str) and wsgi_key.startswith("HTTP_") 59 | 60 | 61 | def unmangle(wsgi_key): 62 | return wsgi_key[5:].replace("_", "-").title() 63 | -------------------------------------------------------------------------------- /debug_toolbar/panels/history/__init__.py: -------------------------------------------------------------------------------- 1 | from debug_toolbar.panels.history.panel import HistoryPanel 2 | 3 | __all__ = ["HistoryPanel"] 4 | -------------------------------------------------------------------------------- /debug_toolbar/panels/history/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class HistoryStoreForm(forms.Form): 5 | """ 6 | Validate params 7 | 8 | store_id: The key for the store instance to be fetched. 9 | """ 10 | 11 | store_id = forms.CharField(widget=forms.HiddenInput()) 12 | exclude_history = forms.BooleanField(widget=forms.HiddenInput(), required=False) 13 | -------------------------------------------------------------------------------- /debug_toolbar/panels/history/panel.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import json 3 | 4 | from django.http.request import RawPostDataException 5 | from django.template.loader import render_to_string 6 | from django.templatetags.static import static 7 | from django.urls import path 8 | from django.utils import timezone 9 | from django.utils.translation import gettext_lazy as _ 10 | 11 | from debug_toolbar.panels import Panel 12 | from debug_toolbar.panels.history import views 13 | from debug_toolbar.panels.history.forms import HistoryStoreForm 14 | 15 | 16 | class HistoryPanel(Panel): 17 | """A panel to display History""" 18 | 19 | is_async = True 20 | title = _("History") 21 | nav_title = _("History") 22 | template = "debug_toolbar/panels/history.html" 23 | 24 | def get_headers(self, request): 25 | headers = super().get_headers(request) 26 | observe_request = self.toolbar.get_observe_request() 27 | store_id = self.toolbar.store_id 28 | if store_id and observe_request(request): 29 | headers["djdt-store-id"] = store_id 30 | return headers 31 | 32 | @property 33 | def enabled(self): 34 | # Do not show the history panel if the panels are rendered on request 35 | # rather than loaded via ajax. 36 | return super().enabled and not self.toolbar.should_render_panels() 37 | 38 | @property 39 | def is_historical(self): 40 | """The HistoryPanel should not be included in the historical panels.""" 41 | return False 42 | 43 | @classmethod 44 | def get_urls(cls): 45 | return [ 46 | path("history_sidebar/", views.history_sidebar, name="history_sidebar"), 47 | path("history_refresh/", views.history_refresh, name="history_refresh"), 48 | ] 49 | 50 | @property 51 | def nav_subtitle(self): 52 | return self.get_stats().get("request_url", "") 53 | 54 | def generate_stats(self, request, response): 55 | try: 56 | if request.method == "GET": 57 | data = request.GET.copy() 58 | else: 59 | data = request.POST.copy() 60 | # GraphQL tends to not be populated in POST. If the request seems 61 | # empty, check if it's a JSON request. 62 | if ( 63 | not data 64 | and request.body 65 | and request.headers.get("content-type") == "application/json" 66 | ): 67 | with contextlib.suppress(ValueError): 68 | data = json.loads(request.body) 69 | 70 | except RawPostDataException: 71 | # It is not guaranteed that we may read the request data (again). 72 | data = None 73 | 74 | self.record_stats( 75 | { 76 | "request_url": request.get_full_path(), 77 | "request_method": request.method, 78 | "status_code": response.status_code, 79 | "data": data, 80 | "time": timezone.now(), 81 | } 82 | ) 83 | 84 | @property 85 | def content(self): 86 | """Content of the panel when it's displayed in full screen. 87 | 88 | Fetch every store for the toolbar and include it in the template. 89 | """ 90 | stores = {} 91 | for id, toolbar in reversed(self.toolbar._store.items()): 92 | stores[id] = { 93 | "toolbar": toolbar, 94 | "form": HistoryStoreForm( 95 | initial={"store_id": id, "exclude_history": True} 96 | ), 97 | } 98 | 99 | return render_to_string( 100 | self.template, 101 | { 102 | "current_store_id": self.toolbar.store_id, 103 | "stores": stores, 104 | "refresh_form": HistoryStoreForm( 105 | initial={ 106 | "store_id": self.toolbar.store_id, 107 | "exclude_history": True, 108 | } 109 | ), 110 | }, 111 | ) 112 | 113 | @property 114 | def scripts(self): 115 | scripts = super().scripts 116 | scripts.append(static("debug_toolbar/js/history.js")) 117 | return scripts 118 | -------------------------------------------------------------------------------- /debug_toolbar/panels/history/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseBadRequest, JsonResponse 2 | from django.template.loader import render_to_string 3 | 4 | from debug_toolbar._compat import login_not_required 5 | from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar 6 | from debug_toolbar.panels.history.forms import HistoryStoreForm 7 | from debug_toolbar.toolbar import DebugToolbar 8 | 9 | 10 | @login_not_required 11 | @require_show_toolbar 12 | @render_with_toolbar_language 13 | def history_sidebar(request): 14 | """Returns the selected debug toolbar history snapshot.""" 15 | form = HistoryStoreForm(request.GET) 16 | 17 | if form.is_valid(): 18 | store_id = form.cleaned_data["store_id"] 19 | toolbar = DebugToolbar.fetch(store_id) 20 | exclude_history = form.cleaned_data["exclude_history"] 21 | context = {} 22 | if toolbar is None: 23 | # When the store_id has been popped already due to 24 | # RESULTS_CACHE_SIZE 25 | return JsonResponse(context) 26 | for panel in toolbar.panels: 27 | if exclude_history and not panel.is_historical: 28 | continue 29 | panel_context = {"panel": panel} 30 | context[panel.panel_id] = { 31 | "button": render_to_string( 32 | "debug_toolbar/includes/panel_button.html", panel_context 33 | ), 34 | "content": render_to_string( 35 | "debug_toolbar/includes/panel_content.html", panel_context 36 | ), 37 | } 38 | return JsonResponse(context) 39 | return HttpResponseBadRequest("Form errors") 40 | 41 | 42 | @login_not_required 43 | @require_show_toolbar 44 | @render_with_toolbar_language 45 | def history_refresh(request): 46 | """Returns the refreshed list of table rows for the History Panel.""" 47 | form = HistoryStoreForm(request.GET) 48 | 49 | if form.is_valid(): 50 | requests = [] 51 | # Convert to list to handle mutations happening in parallel 52 | for id, toolbar in list(DebugToolbar._store.items()): 53 | requests.append( 54 | { 55 | "id": id, 56 | "content": render_to_string( 57 | "debug_toolbar/panels/history_tr.html", 58 | { 59 | "id": id, 60 | "store_context": { 61 | "toolbar": toolbar, 62 | "form": HistoryStoreForm( 63 | initial={ 64 | "store_id": id, 65 | "exclude_history": True, 66 | } 67 | ), 68 | }, 69 | }, 70 | ), 71 | } 72 | ) 73 | 74 | return JsonResponse({"requests": requests}) 75 | return HttpResponseBadRequest("Form errors") 76 | -------------------------------------------------------------------------------- /debug_toolbar/panels/redirects.py: -------------------------------------------------------------------------------- 1 | from inspect import iscoroutine 2 | 3 | from django.template.response import SimpleTemplateResponse 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from debug_toolbar.panels import Panel 7 | 8 | 9 | class RedirectsPanel(Panel): 10 | """ 11 | Panel that intercepts redirects and displays a page with debug info. 12 | """ 13 | 14 | has_content = False 15 | 16 | is_async = True 17 | 18 | nav_title = _("Intercept redirects") 19 | 20 | def _process_response(self, response): 21 | """ 22 | Common response processing logic. 23 | """ 24 | if 300 <= response.status_code < 400: 25 | if redirect_to := response.get("Location"): 26 | response = self.get_interception_response(response, redirect_to) 27 | response.render() 28 | return response 29 | 30 | async def aprocess_request(self, request, response_coroutine): 31 | """ 32 | Async version of process_request. used for accessing the response 33 | by awaiting it when running in ASGI. 34 | """ 35 | 36 | response = await response_coroutine 37 | return self._process_response(response) 38 | 39 | def process_request(self, request): 40 | response = super().process_request(request) 41 | if iscoroutine(response): 42 | return self.aprocess_request(request, response) 43 | return self._process_response(response) 44 | 45 | def get_interception_response(self, response, redirect_to): 46 | """ 47 | Hook method to allow subclasses to customize the interception response. 48 | """ 49 | status_line = f"{response.status_code} {response.reason_phrase}" 50 | cookies = response.cookies 51 | original_response = response 52 | context = { 53 | "redirect_to": redirect_to, 54 | "status_line": status_line, 55 | "toolbar": self.toolbar, 56 | "original_response": original_response, 57 | } 58 | # Using SimpleTemplateResponse avoids running global context processors. 59 | response = SimpleTemplateResponse("debug_toolbar/redirect.html", context) 60 | response.cookies = cookies 61 | response.original_response = original_response 62 | return response 63 | -------------------------------------------------------------------------------- /debug_toolbar/panels/request.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.urls import resolve 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from debug_toolbar.panels import Panel 6 | from debug_toolbar.utils import get_name_from_obj, sanitize_and_sort_request_vars 7 | 8 | 9 | class RequestPanel(Panel): 10 | """ 11 | A panel to display request variables (POST/GET, session, cookies). 12 | """ 13 | 14 | template = "debug_toolbar/panels/request.html" 15 | 16 | title = _("Request") 17 | 18 | @property 19 | def nav_subtitle(self): 20 | """ 21 | Show abbreviated name of view function as subtitle 22 | """ 23 | view_func = self.get_stats().get("view_func", "") 24 | return view_func.rsplit(".", 1)[-1] 25 | 26 | def generate_stats(self, request, response): 27 | self.record_stats( 28 | { 29 | "get": sanitize_and_sort_request_vars(request.GET), 30 | "post": sanitize_and_sort_request_vars(request.POST), 31 | "cookies": sanitize_and_sort_request_vars(request.COOKIES), 32 | } 33 | ) 34 | 35 | view_info = { 36 | "view_func": _(""), 37 | "view_args": "None", 38 | "view_kwargs": "None", 39 | "view_urlname": "None", 40 | } 41 | try: 42 | match = resolve(request.path_info) 43 | func, args, kwargs = match 44 | view_info["view_func"] = get_name_from_obj(func) 45 | view_info["view_args"] = args 46 | view_info["view_kwargs"] = kwargs 47 | 48 | if getattr(match, "url_name", False): 49 | url_name = match.url_name 50 | if match.namespaces: 51 | url_name = ":".join([*match.namespaces, url_name]) 52 | else: 53 | url_name = _("") 54 | 55 | view_info["view_urlname"] = url_name 56 | 57 | except Http404: 58 | pass 59 | self.record_stats(view_info) 60 | 61 | if hasattr(request, "session"): 62 | session_data = dict(request.session) 63 | self.record_stats({"session": sanitize_and_sort_request_vars(session_data)}) 64 | -------------------------------------------------------------------------------- /debug_toolbar/panels/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.translation import gettext_lazy as _ 3 | from django.views.debug import get_default_exception_reporter_filter 4 | 5 | from debug_toolbar.panels import Panel 6 | 7 | get_safe_settings = get_default_exception_reporter_filter().get_safe_settings 8 | 9 | 10 | class SettingsPanel(Panel): 11 | """ 12 | A panel to display all variables in django.conf.settings 13 | """ 14 | 15 | template = "debug_toolbar/panels/settings.html" 16 | 17 | is_async = True 18 | 19 | nav_title = _("Settings") 20 | 21 | def title(self): 22 | return _("Settings from %s") % settings.SETTINGS_MODULE 23 | 24 | def generate_stats(self, request, response): 25 | self.record_stats({"settings": dict(sorted(get_safe_settings().items()))}) 26 | -------------------------------------------------------------------------------- /debug_toolbar/panels/signals.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | from django.core.signals import ( 4 | got_request_exception, 5 | request_finished, 6 | request_started, 7 | setting_changed, 8 | ) 9 | from django.db.backends.signals import connection_created 10 | from django.db.models.signals import ( 11 | class_prepared, 12 | m2m_changed, 13 | post_delete, 14 | post_init, 15 | post_migrate, 16 | post_save, 17 | pre_delete, 18 | pre_init, 19 | pre_migrate, 20 | pre_save, 21 | ) 22 | from django.utils.module_loading import import_string 23 | from django.utils.translation import gettext_lazy as _, ngettext 24 | 25 | from debug_toolbar.panels import Panel 26 | 27 | 28 | class SignalsPanel(Panel): 29 | template = "debug_toolbar/panels/signals.html" 30 | 31 | is_async = True 32 | 33 | SIGNALS = { 34 | "request_started": request_started, 35 | "request_finished": request_finished, 36 | "got_request_exception": got_request_exception, 37 | "connection_created": connection_created, 38 | "class_prepared": class_prepared, 39 | "pre_init": pre_init, 40 | "post_init": post_init, 41 | "pre_save": pre_save, 42 | "post_save": post_save, 43 | "pre_delete": pre_delete, 44 | "post_delete": post_delete, 45 | "m2m_changed": m2m_changed, 46 | "pre_migrate": pre_migrate, 47 | "post_migrate": post_migrate, 48 | "setting_changed": setting_changed, 49 | } 50 | 51 | def nav_subtitle(self): 52 | signals = self.get_stats()["signals"] 53 | num_receivers = sum(len(receivers) for name, receivers in signals) 54 | num_signals = len(signals) 55 | # here we have to handle a double count translation, hence the 56 | # hard coding of one signal 57 | if num_signals == 1: 58 | return ngettext( 59 | "%(num_receivers)d receiver of 1 signal", 60 | "%(num_receivers)d receivers of 1 signal", 61 | num_receivers, 62 | ) % {"num_receivers": num_receivers} 63 | return ngettext( 64 | "%(num_receivers)d receiver of %(num_signals)d signals", 65 | "%(num_receivers)d receivers of %(num_signals)d signals", 66 | num_receivers, 67 | ) % {"num_receivers": num_receivers, "num_signals": num_signals} 68 | 69 | title = _("Signals") 70 | 71 | @property 72 | def signals(self): 73 | signals = self.SIGNALS.copy() 74 | for signal in self.toolbar.config["EXTRA_SIGNALS"]: 75 | signal_name = signal.rsplit(".", 1)[-1] 76 | signals[signal_name] = import_string(signal) 77 | return signals 78 | 79 | def generate_stats(self, request, response): 80 | signals = [] 81 | for name, signal in sorted(self.signals.items()): 82 | receivers = [] 83 | for receiver in signal.receivers: 84 | receiver = receiver[1] 85 | if isinstance(receiver, weakref.ReferenceType): 86 | receiver = receiver() 87 | if receiver is None: 88 | continue 89 | 90 | receiver = getattr(receiver, "__wraps__", receiver) 91 | receiver_name = getattr(receiver, "__name__", str(receiver)) 92 | if getattr(receiver, "__self__", None) is not None: 93 | receiver_class_name = getattr( 94 | receiver.__self__, "__class__", type 95 | ).__name__ 96 | text = f"{receiver_class_name}.{receiver_name}" 97 | else: 98 | text = receiver_name 99 | receivers.append(text) 100 | signals.append((name, receivers)) 101 | 102 | self.record_stats({"signals": signals}) 103 | -------------------------------------------------------------------------------- /debug_toolbar/panels/sql/__init__.py: -------------------------------------------------------------------------------- 1 | from debug_toolbar.panels.sql.panel import SQLPanel 2 | 3 | __all__ = ["SQLPanel"] 4 | -------------------------------------------------------------------------------- /debug_toolbar/panels/sql/forms.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import forms 4 | from django.core.exceptions import ValidationError 5 | from django.db import connections 6 | from django.utils.functional import cached_property 7 | 8 | from debug_toolbar.panels.sql.utils import is_select_query, reformat_sql 9 | 10 | 11 | class SQLSelectForm(forms.Form): 12 | """ 13 | Validate params 14 | 15 | sql: The sql statement with interpolated params 16 | raw_sql: The sql statement with placeholders 17 | params: JSON encoded parameter values 18 | duration: time for SQL to execute passed in from toolbar just for redisplay 19 | """ 20 | 21 | sql = forms.CharField() 22 | raw_sql = forms.CharField() 23 | params = forms.CharField() 24 | alias = forms.CharField(required=False, initial="default") 25 | duration = forms.FloatField() 26 | 27 | def clean_raw_sql(self): 28 | value = self.cleaned_data["raw_sql"] 29 | 30 | if not is_select_query(value): 31 | raise ValidationError("Only 'select' queries are allowed.") 32 | 33 | return value 34 | 35 | def clean_params(self): 36 | value = self.cleaned_data["params"] 37 | 38 | try: 39 | return json.loads(value) 40 | except ValueError as exc: 41 | raise ValidationError("Is not valid JSON") from exc 42 | 43 | def clean_alias(self): 44 | value = self.cleaned_data["alias"] 45 | 46 | if value not in connections: 47 | raise ValidationError(f"Database alias '{value}' not found") 48 | 49 | return value 50 | 51 | def reformat_sql(self): 52 | return reformat_sql(self.cleaned_data["sql"], with_toggle=False) 53 | 54 | @property 55 | def connection(self): 56 | return connections[self.cleaned_data["alias"]] 57 | 58 | @cached_property 59 | def cursor(self): 60 | return self.connection.cursor() 61 | -------------------------------------------------------------------------------- /debug_toolbar/panels/templates/__init__.py: -------------------------------------------------------------------------------- 1 | from debug_toolbar.panels.templates.panel import TemplatesPanel 2 | 3 | __all__ = ["TemplatesPanel"] 4 | -------------------------------------------------------------------------------- /debug_toolbar/panels/templates/jinja2.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from django.template.backends.jinja2 import Template as JinjaTemplate 4 | from django.template.context import make_context 5 | from django.test.signals import template_rendered 6 | 7 | 8 | def patch_jinja_render(): 9 | orig_render = JinjaTemplate.render 10 | 11 | @functools.wraps(orig_render) 12 | def wrapped_render(self, context=None, request=None): 13 | # This patching of render only instruments the rendering 14 | # of the immediate template. It won't include the parent template(s). 15 | self.name = self.template.name 16 | template_rendered.send( 17 | sender=self, template=self, context=make_context(context, request) 18 | ) 19 | return orig_render(self, context, request) 20 | 21 | if JinjaTemplate.render != wrapped_render: 22 | JinjaTemplate.original_render = JinjaTemplate.render 23 | JinjaTemplate.render = wrapped_render 24 | -------------------------------------------------------------------------------- /debug_toolbar/panels/templates/views.py: -------------------------------------------------------------------------------- 1 | from django.core import signing 2 | from django.http import HttpResponseBadRequest, JsonResponse 3 | from django.template import Origin, TemplateDoesNotExist 4 | from django.template.engine import Engine 5 | from django.template.loader import render_to_string 6 | from django.utils.html import format_html, mark_safe 7 | 8 | from debug_toolbar._compat import login_not_required 9 | from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar 10 | 11 | 12 | @login_not_required 13 | @require_show_toolbar 14 | @render_with_toolbar_language 15 | def template_source(request): 16 | """ 17 | Return the source of a template, syntax-highlighted by Pygments if 18 | it's available. 19 | """ 20 | template_origin_name = request.GET.get("template_origin") 21 | if template_origin_name is None: 22 | return HttpResponseBadRequest('"template_origin" key is required') 23 | try: 24 | template_origin_name = signing.loads(template_origin_name) 25 | except Exception: 26 | return HttpResponseBadRequest('"template_origin" is invalid') 27 | template_name = request.GET.get("template", template_origin_name) 28 | 29 | final_loaders = [] 30 | loaders = list(Engine.get_default().template_loaders) 31 | 32 | while loaders: 33 | loader = loaders.pop(0) 34 | 35 | if loader is not None: 36 | # Recursively unwrap loaders until we get to loaders which do not 37 | # themselves wrap other loaders. This adds support for 38 | # django.template.loaders.cached.Loader and the 39 | # django-template-partials loader (possibly among others) 40 | if hasattr(loader, "loaders"): 41 | loaders.extend(loader.loaders) 42 | else: 43 | final_loaders.append(loader) 44 | 45 | for loader in final_loaders: 46 | origin = Origin(template_origin_name) 47 | try: 48 | source = loader.get_contents(origin) 49 | break 50 | except TemplateDoesNotExist: 51 | pass 52 | else: 53 | source = f"Template Does Not Exist: {template_origin_name}" 54 | 55 | try: 56 | from pygments import highlight 57 | from pygments.formatters import HtmlFormatter 58 | from pygments.lexers import HtmlDjangoLexer 59 | except ModuleNotFoundError: 60 | source = format_html("{}", source) 61 | else: 62 | source = highlight(source, HtmlDjangoLexer(), HtmlFormatter(wrapcode=True)) 63 | source = mark_safe(source) 64 | 65 | content = render_to_string( 66 | "debug_toolbar/panels/template_source.html", 67 | {"source": source, "template_name": template_name}, 68 | ) 69 | return JsonResponse({"content": content}) 70 | -------------------------------------------------------------------------------- /debug_toolbar/panels/timer.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | from django.template.loader import render_to_string 4 | from django.templatetags.static import static 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from debug_toolbar.panels import Panel 8 | 9 | try: 10 | import resource # Not available on Win32 systems 11 | except ImportError: 12 | resource = None 13 | 14 | 15 | class TimerPanel(Panel): 16 | """ 17 | Panel that displays the time a response took in milliseconds. 18 | """ 19 | 20 | def nav_subtitle(self): 21 | stats = self.get_stats() 22 | if hasattr(self, "_start_rusage"): 23 | utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime 24 | stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime 25 | return _("CPU: %(cum)0.2fms (%(total)0.2fms)") % { 26 | "cum": (utime + stime) * 1000.0, 27 | "total": stats["total_time"], 28 | } 29 | elif "total_time" in stats: 30 | return _("Total: %0.2fms") % stats["total_time"] 31 | else: 32 | return "" 33 | 34 | has_content = resource is not None 35 | 36 | title = _("Time") 37 | 38 | template = "debug_toolbar/panels/timer.html" 39 | 40 | @property 41 | def content(self): 42 | stats = self.get_stats() 43 | rows = ( 44 | (_("User CPU time"), _("%(utime)0.3f msec") % stats), 45 | (_("System CPU time"), _("%(stime)0.3f msec") % stats), 46 | (_("Total CPU time"), _("%(total)0.3f msec") % stats), 47 | (_("Elapsed time"), _("%(total_time)0.3f msec") % stats), 48 | ( 49 | _("Context switches"), 50 | _("%(vcsw)d voluntary, %(ivcsw)d involuntary") % stats, 51 | ), 52 | ) 53 | return render_to_string(self.template, {"rows": rows}) 54 | 55 | @property 56 | def scripts(self): 57 | scripts = super().scripts 58 | scripts.append(static("debug_toolbar/js/timer.js")) 59 | return scripts 60 | 61 | def process_request(self, request): 62 | self._start_time = perf_counter() 63 | if self.has_content: 64 | self._start_rusage = resource.getrusage(resource.RUSAGE_SELF) 65 | return super().process_request(request) 66 | 67 | def generate_stats(self, request, response): 68 | stats = {} 69 | if hasattr(self, "_start_time"): 70 | stats["total_time"] = (perf_counter() - self._start_time) * 1000 71 | if hasattr(self, "_start_rusage"): 72 | self._end_rusage = resource.getrusage(resource.RUSAGE_SELF) 73 | stats["utime"] = 1000 * self._elapsed_ru("ru_utime") 74 | stats["stime"] = 1000 * self._elapsed_ru("ru_stime") 75 | stats["total"] = stats["utime"] + stats["stime"] 76 | stats["vcsw"] = self._elapsed_ru("ru_nvcsw") 77 | stats["ivcsw"] = self._elapsed_ru("ru_nivcsw") 78 | stats["minflt"] = self._elapsed_ru("ru_minflt") 79 | stats["majflt"] = self._elapsed_ru("ru_majflt") 80 | # these are documented as not meaningful under Linux. If you're 81 | # running BSD feel free to enable them, and add any others that I 82 | # hadn't gotten to before I noticed that I was getting nothing but 83 | # zeroes and that the docs agreed. :-( 84 | # 85 | # stats['blkin'] = self._elapsed_ru('ru_inblock') 86 | # stats['blkout'] = self._elapsed_ru('ru_oublock') 87 | # stats['swap'] = self._elapsed_ru('ru_nswap') 88 | # stats['rss'] = self._end_rusage.ru_maxrss 89 | # stats['srss'] = self._end_rusage.ru_ixrss 90 | # stats['urss'] = self._end_rusage.ru_idrss 91 | # stats['usrss'] = self._end_rusage.ru_isrss 92 | 93 | self.record_stats(stats) 94 | 95 | def generate_server_timing(self, request, response): 96 | stats = self.get_stats() 97 | 98 | self.record_server_timing("utime", "User CPU time", stats.get("utime", 0)) 99 | self.record_server_timing("stime", "System CPU time", stats.get("stime", 0)) 100 | self.record_server_timing("total", "Total CPU time", stats.get("total", 0)) 101 | self.record_server_timing( 102 | "total_time", "Elapsed time", stats.get("total_time", 0) 103 | ) 104 | 105 | def _elapsed_ru(self, name): 106 | return getattr(self._end_rusage, name) - getattr(self._start_rusage, name) 107 | -------------------------------------------------------------------------------- /debug_toolbar/panels/versions.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import django 4 | from django.apps import apps 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from debug_toolbar.panels import Panel 8 | 9 | 10 | class VersionsPanel(Panel): 11 | """ 12 | Shows versions of Python, Django, and installed apps if possible. 13 | """ 14 | 15 | is_async = True 16 | 17 | @property 18 | def nav_subtitle(self): 19 | return f"Django {django.get_version()}" 20 | 21 | title = _("Versions") 22 | 23 | template = "debug_toolbar/panels/versions.html" 24 | 25 | def generate_stats(self, request, response): 26 | versions = [ 27 | ("Python", "", "%d.%d.%d" % sys.version_info[:3]), 28 | ("Django", "", self.get_app_version(django)), 29 | ] 30 | versions += list(self.gen_app_versions()) 31 | self.record_stats( 32 | {"versions": sorted(versions, key=lambda v: v[0]), "paths": sys.path} 33 | ) 34 | 35 | def gen_app_versions(self): 36 | for app_config in apps.get_app_configs(): 37 | name = app_config.verbose_name 38 | app = app_config.module 39 | version = self.get_app_version(app) 40 | if version: 41 | yield app.__name__, name, version 42 | 43 | def get_app_version(self, app): 44 | version = self.get_version_from_app(app) 45 | if isinstance(version, (list, tuple)): 46 | # We strip dots from the right because we do not want to show 47 | # trailing dots if there are empty elements in the list/tuple 48 | version = ".".join(str(o) for o in version).rstrip(".") 49 | return version 50 | 51 | def get_version_from_app(self, app): 52 | if hasattr(app, "get_version"): 53 | get_version = app.get_version 54 | if callable(get_version): 55 | try: 56 | return get_version() 57 | except TypeError: 58 | pass 59 | else: 60 | return get_version 61 | if hasattr(app, "VERSION"): 62 | return app.VERSION 63 | if hasattr(app, "__version__"): 64 | return app.__version__ 65 | return 66 | -------------------------------------------------------------------------------- /debug_toolbar/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import warnings 4 | from functools import cache 5 | 6 | from django.conf import settings 7 | from django.dispatch import receiver 8 | from django.test.signals import setting_changed 9 | 10 | 11 | def _is_running_tests(): 12 | """ 13 | Helper function to support testing default value for 14 | IS_RUNNING_TESTS 15 | """ 16 | return "test" in sys.argv or "PYTEST_VERSION" in os.environ 17 | 18 | 19 | CONFIG_DEFAULTS = { 20 | # Toolbar options 21 | "DISABLE_PANELS": { 22 | "debug_toolbar.panels.profiling.ProfilingPanel", 23 | "debug_toolbar.panels.redirects.RedirectsPanel", 24 | }, 25 | "INSERT_BEFORE": "", 26 | "RENDER_PANELS": None, 27 | "RESULTS_CACHE_SIZE": 25, 28 | "ROOT_TAG_EXTRA_ATTRS": "", 29 | "SHOW_COLLAPSED": False, 30 | "SHOW_TOOLBAR_CALLBACK": "debug_toolbar.middleware.show_toolbar", 31 | # Panel options 32 | "EXTRA_SIGNALS": [], 33 | "ENABLE_STACKTRACES": True, 34 | "ENABLE_STACKTRACES_LOCALS": False, 35 | "HIDE_IN_STACKTRACES": ( 36 | "socketserver", 37 | "threading", 38 | "wsgiref", 39 | "debug_toolbar", 40 | "django.db", 41 | "django.core.handlers", 42 | "django.core.servers", 43 | "django.utils.decorators", 44 | "django.utils.deprecation", 45 | "django.utils.functional", 46 | ), 47 | "PRETTIFY_SQL": True, 48 | "PROFILER_CAPTURE_PROJECT_CODE": True, 49 | "PROFILER_MAX_DEPTH": 10, 50 | "PROFILER_THRESHOLD_RATIO": 8, 51 | "SHOW_TEMPLATE_CONTEXT": True, 52 | "SKIP_TEMPLATE_PREFIXES": ("django/forms/widgets/", "admin/widgets/"), 53 | "SQL_WARNING_THRESHOLD": 500, # milliseconds 54 | "OBSERVE_REQUEST_CALLBACK": "debug_toolbar.toolbar.observe_request", 55 | "TOOLBAR_LANGUAGE": None, 56 | "IS_RUNNING_TESTS": _is_running_tests(), 57 | "UPDATE_ON_FETCH": False, 58 | } 59 | 60 | 61 | @cache 62 | def get_config(): 63 | USER_CONFIG = getattr(settings, "DEBUG_TOOLBAR_CONFIG", {}) 64 | CONFIG = CONFIG_DEFAULTS.copy() 65 | CONFIG.update(USER_CONFIG) 66 | return CONFIG 67 | 68 | 69 | PANELS_DEFAULTS = [ 70 | "debug_toolbar.panels.history.HistoryPanel", 71 | "debug_toolbar.panels.versions.VersionsPanel", 72 | "debug_toolbar.panels.timer.TimerPanel", 73 | "debug_toolbar.panels.settings.SettingsPanel", 74 | "debug_toolbar.panels.headers.HeadersPanel", 75 | "debug_toolbar.panels.request.RequestPanel", 76 | "debug_toolbar.panels.sql.SQLPanel", 77 | "debug_toolbar.panels.staticfiles.StaticFilesPanel", 78 | "debug_toolbar.panels.templates.TemplatesPanel", 79 | "debug_toolbar.panels.alerts.AlertsPanel", 80 | "debug_toolbar.panels.cache.CachePanel", 81 | "debug_toolbar.panels.signals.SignalsPanel", 82 | "debug_toolbar.panels.redirects.RedirectsPanel", 83 | "debug_toolbar.panels.profiling.ProfilingPanel", 84 | ] 85 | 86 | 87 | @cache 88 | def get_panels(): 89 | try: 90 | PANELS = list(settings.DEBUG_TOOLBAR_PANELS) 91 | except AttributeError: 92 | PANELS = PANELS_DEFAULTS 93 | 94 | logging_panel = "debug_toolbar.panels.logging.LoggingPanel" 95 | if logging_panel in PANELS: 96 | PANELS = [panel for panel in PANELS if panel != logging_panel] 97 | warnings.warn( 98 | f"Please remove {logging_panel} from your DEBUG_TOOLBAR_PANELS setting.", 99 | DeprecationWarning, 100 | stacklevel=1, 101 | ) 102 | return PANELS 103 | 104 | 105 | @receiver(setting_changed) 106 | def update_toolbar_config(*, setting, **kwargs): 107 | """ 108 | Refresh configuration when overriding settings. 109 | """ 110 | if setting == "DEBUG_TOOLBAR_CONFIG": 111 | get_config.cache_clear() 112 | elif setting == "DEBUG_TOOLBAR_PANELS": 113 | from debug_toolbar.toolbar import DebugToolbar 114 | 115 | get_panels.cache_clear() 116 | DebugToolbar._panel_classes = None 117 | # Not implemented: invalidate debug_toolbar.urls. 118 | -------------------------------------------------------------------------------- /debug_toolbar/static/debug_toolbar/css/print.css: -------------------------------------------------------------------------------- 1 | #djDebug { 2 | display: none !important; 3 | } 4 | -------------------------------------------------------------------------------- /debug_toolbar/static/debug_toolbar/js/history.js: -------------------------------------------------------------------------------- 1 | import { $$, ajaxForm, replaceToolbarState } from "./utils.js"; 2 | 3 | const djDebug = document.getElementById("djDebug"); 4 | 5 | function difference(setA, setB) { 6 | const _difference = new Set(setA); 7 | for (const elem of setB) { 8 | _difference.delete(elem); 9 | } 10 | return _difference; 11 | } 12 | 13 | /** 14 | * Create an array of dataset properties from a NodeList. 15 | */ 16 | function pluckData(nodes, key) { 17 | return [...nodes].map((obj) => obj.dataset[key]); 18 | } 19 | 20 | function refreshHistory() { 21 | const formTarget = djDebug.querySelector(".refreshHistory"); 22 | const container = document.getElementById("djdtHistoryRequests"); 23 | const oldIds = new Set( 24 | pluckData(container.querySelectorAll("tr[data-store-id]"), "storeId") 25 | ); 26 | 27 | ajaxForm(formTarget) 28 | .then((data) => { 29 | // Remove existing rows first then re-populate with new data 30 | for (const node of container.querySelectorAll( 31 | "tr[data-store-id]" 32 | )) { 33 | node.remove(); 34 | } 35 | for (const request of data.requests) { 36 | container.innerHTML = request.content + container.innerHTML; 37 | } 38 | }) 39 | .then(() => { 40 | const allIds = new Set( 41 | pluckData( 42 | container.querySelectorAll("tr[data-store-id]"), 43 | "storeId" 44 | ) 45 | ); 46 | const newIds = difference(allIds, oldIds); 47 | const lastRequestId = newIds.values().next().value; 48 | return { 49 | allIds, 50 | newIds, 51 | lastRequestId, 52 | }; 53 | }) 54 | .then((refreshInfo) => { 55 | for (const newId of refreshInfo.newIds) { 56 | const row = container.querySelector( 57 | `tr[data-store-id="${newId}"]` 58 | ); 59 | row.classList.add("flash-new"); 60 | } 61 | setTimeout(() => { 62 | for (const row of container.querySelectorAll( 63 | "tr[data-store-id]" 64 | )) { 65 | row.classList.remove("flash-new"); 66 | } 67 | }, 2000); 68 | }); 69 | } 70 | 71 | function switchHistory(newStoreId) { 72 | const formTarget = djDebug.querySelector( 73 | `.switchHistory[data-store-id='${newStoreId}']` 74 | ); 75 | const tbody = formTarget.closest("tbody"); 76 | 77 | const highlighted = tbody.querySelector(".djdt-highlighted"); 78 | if (highlighted) { 79 | highlighted.classList.remove("djdt-highlighted"); 80 | } 81 | formTarget.closest("tr").classList.add("djdt-highlighted"); 82 | 83 | ajaxForm(formTarget).then((data) => { 84 | if (Object.keys(data).length === 0) { 85 | const container = document.getElementById("djdtHistoryRequests"); 86 | container.querySelector( 87 | `button[data-store-id="${newStoreId}"]` 88 | ).innerHTML = "Switch [EXPIRED]"; 89 | } 90 | replaceToolbarState(newStoreId, data); 91 | }); 92 | } 93 | 94 | $$.on(djDebug, "click", ".switchHistory", function (event) { 95 | event.preventDefault(); 96 | switchHistory(this.dataset.storeId); 97 | }); 98 | 99 | $$.on(djDebug, "click", ".refreshHistory", (event) => { 100 | event.preventDefault(); 101 | refreshHistory(); 102 | }); 103 | // We don't refresh the whole toolbar each fetch or ajax request, 104 | // so we need to refresh the history when we open the panel 105 | $$.onPanelRender(djDebug, "HistoryPanel", refreshHistory); 106 | -------------------------------------------------------------------------------- /debug_toolbar/static/debug_toolbar/js/redirect.js: -------------------------------------------------------------------------------- 1 | document.getElementById("redirect_to").focus(); 2 | -------------------------------------------------------------------------------- /debug_toolbar/static/debug_toolbar/js/timer.js: -------------------------------------------------------------------------------- 1 | import { $$ } from "./utils.js"; 2 | 3 | function insertBrowserTiming() { 4 | const timingOffset = performance.timing.navigationStart; 5 | const timingEnd = performance.timing.loadEventEnd; 6 | const totalTime = timingEnd - timingOffset; 7 | function getLeft(stat) { 8 | if (totalTime !== 0) { 9 | return ( 10 | ((performance.timing[stat] - timingOffset) / totalTime) * 100.0 11 | ); 12 | } 13 | return 0; 14 | } 15 | function getCSSWidth(stat, endStat) { 16 | let width = 0; 17 | if (totalTime !== 0) { 18 | width = 19 | ((performance.timing[endStat] - performance.timing[stat]) / 20 | totalTime) * 21 | 100.0; 22 | } 23 | const denominator = 100.0 - getLeft(stat); 24 | if (denominator !== 0) { 25 | // Calculate relative percent (same as sql panel logic) 26 | width = (100.0 * width) / denominator; 27 | } else { 28 | width = 0; 29 | } 30 | return width < 1 ? "2px" : `${width}%`; 31 | } 32 | function addRow(tbody, stat, endStat) { 33 | const row = document.createElement("tr"); 34 | const elapsed = performance.timing[stat] - timingOffset; 35 | if (endStat) { 36 | const duration = 37 | performance.timing[endStat] - performance.timing[stat]; 38 | // Render a start through end bar 39 | row.innerHTML = ` 40 | ${stat.replace("Start", "")} 41 | 42 | ${elapsed} (+${duration}) 43 | `; 44 | row.querySelector("rect").setAttribute( 45 | "width", 46 | getCSSWidth(stat, endStat) 47 | ); 48 | } else { 49 | // Render a point in time 50 | row.innerHTML = ` 51 | ${stat} 52 | 53 | ${elapsed} 54 | `; 55 | row.querySelector("rect").setAttribute("width", 2); 56 | } 57 | row.querySelector("rect").setAttribute("x", getLeft(stat)); 58 | tbody.appendChild(row); 59 | } 60 | 61 | const browserTiming = document.getElementById("djDebugBrowserTiming"); 62 | // Determine if the browser timing section has already been rendered. 63 | if (browserTiming.classList.contains("djdt-hidden")) { 64 | const tbody = document.getElementById("djDebugBrowserTimingTableBody"); 65 | // This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param) 66 | addRow(tbody, "domainLookupStart", "domainLookupEnd"); 67 | addRow(tbody, "connectStart", "connectEnd"); 68 | addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd 69 | addRow(tbody, "responseStart", "responseEnd"); 70 | addRow(tbody, "domLoading", "domComplete"); // Spans the events below 71 | addRow(tbody, "domInteractive"); 72 | addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); 73 | addRow(tbody, "loadEventStart", "loadEventEnd"); 74 | browserTiming.classList.remove("djdt-hidden"); 75 | } 76 | } 77 | 78 | const djDebug = document.getElementById("djDebug"); 79 | // Insert the browser timing now since it's possible for this 80 | // script to miss the initial panel load event. 81 | insertBrowserTiming(); 82 | $$.onPanelRender(djDebug, "TimerPanel", insertBrowserTiming); 83 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | {% block css %} 3 | 4 | 5 | {% endblock css %} 6 | {% block js %} 7 | 8 | {% endblock js %} 9 |
20 |
21 | 32 |
33 |
34 |
35 | DJDT 36 |
37 |
38 | 39 | {% for panel in toolbar.panels %} 40 | {% include "debug_toolbar/includes/panel_content.html" %} 41 | {% endfor %} 42 |
43 |
44 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/includes/panel_button.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
  • 4 | 5 | {% if panel.has_content and panel.enabled %} 6 | 7 | {% else %} 8 | 20 | {% endif %} 21 |
  • 22 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/includes/panel_content.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | {% if panel.has_content and panel.enabled %} 4 |
    5 |
    6 |

    {{ panel.title }}

    7 | 8 |
    9 |
    10 | {% if toolbar.should_render_panels %} 11 | {% for script in panel.scripts %}{% endfor %} 12 |
    {{ panel.content }}
    13 | {% else %} 14 |
    15 |
    16 | {% endif %} 17 |
    18 |
    19 | {% endif %} 20 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/includes/theme_selector.html: -------------------------------------------------------------------------------- 1 | 13 | 27 | 42 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/alerts.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% if alerts %} 4 |

    {% translate "Alerts found" %}

    5 | {% for alert in alerts %} 6 |
      7 |
    • {{ alert.alert }}
    • 8 |
    9 | {% endfor %} 10 | {% else %} 11 |

    {% translate "No alerts found" %}

    12 | {% endif %} 13 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/cache.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

    {% translate "Summary" %}

    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    {% translate "Total calls" %}{% translate "Total time" %}{% translate "Cache hits" %}{% translate "Cache misses" %}
    {{ total_calls }}{{ total_time }} ms{{ hits }}{{ misses }}
    21 |

    {% translate "Commands" %}

    22 | 23 | 24 | 25 | {% for name in counts.keys %} 26 | 27 | {% endfor %} 28 | 29 | 30 | 31 | 32 | {% for value in counts.values %} 33 | 34 | {% endfor %} 35 | 36 | 37 |
    {{ name }}
    {{ value }}
    38 | {% if calls %} 39 |

    {% translate "Calls" %}

    40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% for call in calls %} 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {% endfor %} 67 | 68 |
    {% translate "Time (ms)" %}{% translate "Type" %}{% translate "Arguments" %}{% translate "Keyword arguments" %}{% translate "Backend" %}
    54 | 55 | {{ call.time|floatformat:"4" }}{{ call.name|escape }}{{ call.args|escape }}{{ call.kwargs|escape }}{{ call.backend }}
    {{ call.trace }}
    69 | {% endif %} 70 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/headers.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |

    {% translate "Request headers" %}

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for key, value in request_headers.items %} 14 | 15 | 16 | 17 | 18 | {% endfor %} 19 | 20 |
    {% translate "Key" %}{% translate "Value" %}
    {{ key|escape }}{{ value|escape }}
    21 | 22 |

    {% translate "Response headers" %}

    23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for key, value in response_headers.items %} 33 | 34 | 35 | 36 | 37 | {% endfor %} 38 | 39 |
    {% translate "Key" %}{% translate "Value" %}
    {{ key|escape }}{{ value|escape }}
    40 | 41 |

    {% translate "WSGI environ" %}

    42 | 43 |

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

    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {% for key, value in environ.items %} 54 | 55 | 56 | 57 | 58 | {% endfor %} 59 | 60 |
    {% translate "Key" %}{% translate "Value" %}
    {{ key|escape }}{{ value|escape }}
    61 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/history.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 |
    3 | {{ refresh_form.as_div }} 4 | 5 |
    6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for id, store_context in stores.items %} 19 | {% include "debug_toolbar/panels/history_tr.html" %} 20 | {% endfor %} 21 | 22 |
    {% translate "Time" %}{% translate "Method" %}{% translate "Path" %}{% translate "Request Variables" %}{% translate "Status" %}{% translate "Action" %}
    23 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/history_tr.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | {{ store_context.toolbar.stats.HistoryPanel.time|escape }} 5 | 6 | 7 |

    {{ store_context.toolbar.stats.HistoryPanel.request_method|escape }}

    8 | 9 | 10 |

    {{ store_context.toolbar.stats.HistoryPanel.request_url|truncatechars:100|escape }}

    11 | 12 | 13 | 14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for key, value in store_context.toolbar.stats.HistoryPanel.data.items %} 28 | 29 | 30 | 31 | 32 | {% empty %} 33 | 34 | 35 | 36 | {% endfor %} 37 | 38 |
    {% translate "Variable" %}{% translate "Value" %}
    {{ key|pprint }}{{ value|pprint }}
    No data
    39 |
    40 | 41 | 42 |

    {{ store_context.toolbar.stats.HistoryPanel.status_code|escape }}

    43 | 44 | 45 |
    46 | {{ store_context.form.as_div }} 47 | 48 |
    49 | 50 | 51 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/profiling.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for call in func_list %} 15 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 |
    {% translate "Call" %}{% translate "CumTime" %}{% translate "Per" %}{% translate "TotTime" %}{% translate "Per" %}{% translate "Count" %}
    17 |
    18 | {% if call.has_subfuncs %} 19 | 20 | {% else %} 21 | 22 | {% endif %} 23 | {{ call.func_std_string }} 24 |
    25 |
    {{ call.cumtime|floatformat:3 }}{{ call.cumtime_per_call|floatformat:3 }}{{ call.tottime|floatformat:3 }}{{ call.tottime_per_call|floatformat:3 }}{{ call.count }}
    35 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/request.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |

    {% translate "View information" %}

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
    {% translate "View function" %}{% translate "Arguments" %}{% translate "Keyword arguments" %}{% translate "URL name" %}
    {{ view_func }}{{ view_args|pprint }}{{ view_kwargs|pprint }}{{ view_urlname }}
    22 | 23 | {% if cookies.list or cookies.raw %} 24 |

    {% translate "Cookies" %}

    25 | {% include 'debug_toolbar/panels/request_variables.html' with variables=cookies %} 26 | {% else %} 27 |

    {% translate "No cookies" %}

    28 | {% endif %} 29 | 30 | {% if session.list or session.raw %} 31 |

    {% translate "Session data" %}

    32 | {% include 'debug_toolbar/panels/request_variables.html' with variables=session %} 33 | {% else %} 34 |

    {% translate "No session data" %}

    35 | {% endif %} 36 | 37 | {% if get.list or get.raw %} 38 |

    {% translate "GET data" %}

    39 | {% include 'debug_toolbar/panels/request_variables.html' with variables=get %} 40 | {% else %} 41 |

    {% translate "No GET data" %}

    42 | {% endif %} 43 | 44 | {% if post.list or post.raw %} 45 |

    {% translate "POST data" %}

    46 | {% include 'debug_toolbar/panels/request_variables.html' with variables=post %} 47 | {% else %} 48 |

    {% translate "No POST data" %}

    49 | {% endif %} 50 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/request_variables.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% if variables.list %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for key, value in variables.list %} 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 | 23 |
    {% translate "Variable" %}{% translate "Value" %}
    {{ key|pprint }}{{ value|pprint }}
    24 | {% elif variables.raw %} 25 | {{ variables.raw|pprint }} 26 | {% endif %} 27 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/settings.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for name, value in settings.items %} 11 | 12 | 13 | 14 | 15 | {% endfor %} 16 | 17 |
    {% translate "Setting" %}{% translate "Value" %}
    {{ name }}{{ value|pprint }}
    18 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/signals.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for name, receivers in signals %} 11 | 12 | 13 | 14 | 15 | {% endfor %} 16 | 17 |
    {% translate "Signal" %}{% translate "Receivers" %}
    {{ name|escape }}{{ receivers|join:", " }}
    18 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/sql_explain.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
    3 |

    {% translate "SQL explained" %}

    4 | 5 |
    6 |
    7 |
    8 |
    9 |
    {% translate "Executed SQL" %}
    10 |
    {{ sql|safe }}
    11 |
    {% translate "Time" %}
    12 |
    {{ duration }} ms
    13 |
    {% translate "Database" %}
    14 |
    {{ alias }}
    15 |
    16 | 17 | 18 | 19 | {% for h in headers %} 20 | 21 | {% endfor %} 22 | 23 | 24 | 25 | {% for row in result %} 26 | 27 | {% for column in row %} 28 | 29 | {% endfor %} 30 | 31 | {% endfor %} 32 | 33 |
    {{ h|upper }}
    {% if forloop.last %}{% endif %}{{ column|escape }}{% if forloop.last %}{% endif %}
    34 |
    35 |
    36 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/sql_profile.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
    3 |

    {% translate "SQL profiled" %}

    4 | 5 |
    6 |
    7 |
    8 | {% if result %} 9 |
    10 |
    {% translate "Executed SQL" %}
    11 |
    {{ sql|safe }}
    12 |
    {% translate "Time" %}
    13 |
    {{ duration }} ms
    14 |
    {% translate "Database" %}
    15 |
    {{ alias }}
    16 |
    17 | 18 | 19 | 20 | {% for h in headers %} 21 | 22 | {% endfor %} 23 | 24 | 25 | 26 | {% for row in result %} 27 | 28 | {% for column in row %} 29 | 30 | {% endfor %} 31 | 32 | {% endfor %} 33 | 34 |
    {{ h|upper }}
    {{ column|escape }}
    35 | {% else %} 36 |
    37 |
    {% translate "Error" %}
    38 |
    {{ result_error }}
    39 |
    40 | {% endif %} 41 |
    42 |
    43 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/sql_select.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
    3 |

    {% translate "SQL selected" %}

    4 | 5 |
    6 |
    7 |
    8 |
    9 |
    {% translate "Executed SQL" %}
    10 |
    {{ sql|safe }}
    11 |
    {% translate "Time" %}
    12 |
    {{ duration }} ms
    13 |
    {% translate "Database" %}
    14 |
    {{ alias }}
    15 |
    16 | {% if result %} 17 | 18 | 19 | 20 | {% for h in headers %} 21 | 22 | {% endfor %} 23 | 24 | 25 | 26 | {% for row in result %} 27 | 28 | {% for column in row %} 29 | 30 | {% endfor %} 31 | 32 | {% endfor %} 33 | 34 |
    {{ h|upper }}
    {{ column|escape }}
    35 | {% else %} 36 |

    {% translate "Empty set" %}

    37 | {% endif %} 38 |
    39 |
    40 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/staticfiles.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |

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

    4 | {% if staticfiles_dirs %} 5 |
      6 | {% for prefix, staticfiles_dir in staticfiles_dirs %} 7 |
    1. {{ staticfiles_dir }}{% if prefix %} {% blocktranslate %}(prefix {{ prefix }}){% endblocktranslate %}{% endif %}
    2. 8 | {% endfor %} 9 |
    10 | {% else %} 11 |

    {% translate "None" %}

    12 | {% endif %} 13 | 14 |

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

    15 | {% if staticfiles_apps %} 16 |
      17 | {% for static_app in staticfiles_apps %} 18 |
    1. {{ static_app }}
    2. 19 | {% endfor %} 20 |
    21 | {% else %} 22 |

    {% translate "None" %}

    23 | {% endif %} 24 | 25 |

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

    26 | {% if staticfiles %} 27 |
    28 | {% for staticfile in staticfiles %} 29 |
    {{ staticfile }}
    30 |
    {{ staticfile.real_path }}
    31 | {% endfor %} 32 |
    33 | {% else %} 34 |

    {% translate "None" %}

    35 | {% endif %} 36 | 37 | 38 | {% for finder, payload in staticfiles_finders.items %} 39 |

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

    40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for path, real_path in payload %} 49 | 50 | 51 | 52 | 53 | {% endfor %} 54 | 55 |
    {% translate 'Path' %}{% translate 'Location' %}
    {{ path }}{{ real_path }}
    56 | {% endfor %} 57 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/template_source.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
    3 |

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

    4 | 5 |
    6 |
    7 |
    8 | {{ source }} 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/templates.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

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

    3 | {% if template_dirs %} 4 |
      5 | {% for template in template_dirs %} 6 |
    1. {{ template }}
    2. 7 | {% endfor %} 8 |
    9 | {% else %} 10 |

    {% translate "None" %}

    11 | {% endif %} 12 | 13 |

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

    14 | {% if templates %} 15 |
    16 | {% for template in templates %} 17 |
    {{ template.template.name|addslashes }}
    18 |
    {{ template.template.origin_name|addslashes }}
    19 | {% if template.context %} 20 |
    21 |
    22 | {% translate "Toggle context" %} 23 | {{ template.context }} 24 |
    25 |
    26 | {% endif %} 27 | {% endfor %} 28 |
    29 | {% else %} 30 |

    {% translate "None" %}

    31 | {% endif %} 32 | 33 |

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

    34 | {% if context_processors %} 35 |
    36 | {% for key, value in context_processors.items %} 37 |
    {{ key|escape }}
    38 |
    39 |
    40 | {% translate "Toggle context" %} 41 | {{ value|escape }} 42 |
    43 |
    44 | {% endfor %} 45 |
    46 | {% else %} 47 |

    {% translate "None" %}

    48 | {% endif %} 49 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/timer.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

    {% translate "Resource usage" %}

    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for key, value in rows %} 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 | 22 |
    {% translate "Resource" %}{% translate "Value" %}
    {{ key|escape }}{{ value|escape }}
    23 | 24 | 25 |
    26 |

    {% translate "Browser timing" %}

    27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
    {% translate "Timing attribute" %}{% translate "Timeline" %}{% translate "Milliseconds since navigation start (+length)" %}
    43 |
    44 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/panels/versions.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for package, name, version in versions %} 17 | 18 | 19 | 20 | 21 | 22 | {% endfor %} 23 | 24 |
    {% translate "Package" %}{% translate "Name" %}{% translate "Version" %}
    {{ package }}{{ name }}{{ version }}
    25 | -------------------------------------------------------------------------------- /debug_toolbar/templates/debug_toolbar/redirect.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 3 | 4 | 5 | Django Debug Toolbar Redirects Panel: {{ status_line }} 6 | 7 | 8 | 9 |

    {{ status_line }}

    10 |

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

    11 |

    12 | {% 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." %} 13 |

    14 | 15 | 16 | -------------------------------------------------------------------------------- /debug_toolbar/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/debug_toolbar/templatetags/__init__.py -------------------------------------------------------------------------------- /debug_toolbar/urls.py: -------------------------------------------------------------------------------- 1 | from debug_toolbar import APP_NAME 2 | from debug_toolbar.toolbar import DebugToolbar 3 | 4 | app_name = APP_NAME 5 | urlpatterns = DebugToolbar.get_urls() 6 | -------------------------------------------------------------------------------- /debug_toolbar/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.utils.html import escape 3 | from django.utils.translation import gettext as _ 4 | 5 | from debug_toolbar._compat import login_not_required 6 | from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar 7 | from debug_toolbar.toolbar import DebugToolbar 8 | 9 | 10 | @login_not_required 11 | @require_show_toolbar 12 | @render_with_toolbar_language 13 | def render_panel(request): 14 | """Render the contents of a panel""" 15 | toolbar = DebugToolbar.fetch(request.GET["store_id"]) 16 | if toolbar is None: 17 | content = _( 18 | "Data for this panel isn't available anymore. " 19 | "Please reload the page and retry." 20 | ) 21 | content = f"

    {escape(content)}

    " 22 | scripts = [] 23 | else: 24 | panel = toolbar.get_panel_by_id(request.GET["panel_id"]) 25 | content = panel.content 26 | scripts = panel.scripts 27 | return JsonResponse({"content": content, "scripts": scripts}) 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= -n -W 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/architecture.rst: -------------------------------------------------------------------------------- 1 | Architecture 2 | ============ 3 | 4 | The Django Debug Toolbar is designed to be flexible and extensible for 5 | developers and third-party panel creators. 6 | 7 | Core Components 8 | --------------- 9 | 10 | While there are several components, the majority of logic and complexity 11 | lives within the following: 12 | 13 | - ``debug_toolbar.middleware.DebugToolbarMiddleware`` 14 | - ``debug_toolbar.toolbar.DebugToolbar`` 15 | - ``debug_toolbar.panels`` 16 | 17 | ^^^^^^^^^^^^^^^^^^^^^^ 18 | DebugToolbarMiddleware 19 | ^^^^^^^^^^^^^^^^^^^^^^ 20 | 21 | The middleware is how the toolbar integrates with Django projects. 22 | It determines if the toolbar should instrument the request, which 23 | panels to use, facilitates the processing of the request and augmenting 24 | the response with the toolbar. Most logic for how the toolbar interacts 25 | with the user's Django project belongs here. 26 | 27 | ^^^^^^^^^^^^ 28 | DebugToolbar 29 | ^^^^^^^^^^^^ 30 | 31 | The ``DebugToolbar`` class orchestrates the processing of a request 32 | for each of the panels. It contains the logic that needs to be aware 33 | of all the panels, but doesn't need to interact with the user's Django 34 | project. 35 | 36 | ^^^^^^ 37 | Panels 38 | ^^^^^^ 39 | 40 | The majority of the complex logic lives within the panels themselves. This 41 | is because the panels are responsible for collecting the various metrics. 42 | Some of the metrics are collected via 43 | `monkey-patching `_, such as 44 | ``TemplatesPanel``. Others, such as ``SettingsPanel`` don't need to collect 45 | anything and include the data directly in the response. 46 | 47 | Some panels such as ``SQLPanel`` have additional functionality. This tends 48 | to involve a user clicking on something, and the toolbar presenting a new 49 | page with additional data. That additional data is handled in views defined 50 | in the panels package (for example, ``debug_toolbar.panels.sql.views``). 51 | 52 | Logic Flow 53 | ---------- 54 | 55 | When a request comes in, the toolbar first interacts with it in the 56 | middleware. If the middleware determines the request should be instrumented, 57 | it will instantiate the toolbar and pass the request for processing. The 58 | toolbar will use the enabled panels to collect information on the request 59 | and/or response. When the toolbar has completed collecting its metrics on 60 | both the request and response, the middleware will collect the results 61 | from the toolbar. It will inject the HTML and JavaScript to render the 62 | toolbar as well as any headers into the response. 63 | 64 | After the browser renders the panel and the user interacts with it, the 65 | toolbar's JavaScript will send requests to the server. If the view handling 66 | the request needs to fetch data from the toolbar, the request must supply 67 | the store ID. This is so that the toolbar can load the collected metrics 68 | for that particular request. 69 | 70 | The history panel allows a user to view the metrics for any request since 71 | the application was started. The toolbar maintains its state entirely in 72 | memory for the process running ``runserver``. If the application is 73 | restarted the toolbar will lose its state. 74 | 75 | Problematic Parts 76 | ----------------- 77 | 78 | - ``debug.panels.templates.panel``: This monkey-patches template rendering 79 | when the panel module is loaded 80 | - ``debug.panels.sql``: This package is particularly complex, but provides 81 | the main benefit of the toolbar 82 | - Support for async and multi-threading: ``debug_toolbar.middleware.DebugToolbarMiddleware`` 83 | is now async compatible and can process async requests. However certain 84 | panels such as ``TimerPanel``, ``RequestPanel`` and ``ProfilingPanel`` aren't 85 | fully compatible and currently being worked on. For now, these panels 86 | are disabled by default when running in async environment. 87 | follow the progress of this issue in `Async compatible toolbar project `_. 88 | -------------------------------------------------------------------------------- /docs/checks.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | System checks 3 | ============= 4 | 5 | The following :external:doc:`system checks ` help verify the 6 | Django Debug Toolbar setup and configuration: 7 | 8 | * **debug_toolbar.W001**: ``debug_toolbar.middleware.DebugToolbarMiddleware`` 9 | is missing from ``MIDDLEWARE``. 10 | * **debug_toolbar.W002**: ``debug_toolbar.middleware.DebugToolbarMiddleware`` 11 | occurs multiple times in ``MIDDLEWARE``. 12 | * **debug_toolbar.W003**: ``debug_toolbar.middleware.DebugToolbarMiddleware`` 13 | occurs before ``django.middleware.gzip.GZipMiddleware`` in ``MIDDLEWARE``. 14 | * **debug_toolbar.W004**: ``debug_toolbar`` is incompatible with 15 | ``MIDDLEWARE_CLASSES`` setting. 16 | * **debug_toolbar.W005**: Setting ``DEBUG_TOOLBAR_PANELS`` is empty. 17 | * **debug_toolbar.W006**: At least one ``DjangoTemplates`` ``TEMPLATES`` 18 | configuration needs to have 19 | ``django.template.loaders.app_directories.Loader`` included in 20 | ``["OPTIONS"]["loaders"]`` or ``APP_DIRS`` set to ``True``. 21 | * **debug_toolbar.W007**: JavaScript files are resolving to the wrong content 22 | type. Refer to :external:ref:`Django's explanation of 23 | mimetypes on Windows `. 24 | * **debug_toolbar.W008**: The deprecated ``OBSERVE_REQUEST_CALLBACK`` setting 25 | is present in ``DEBUG_TOOLBAR_CONFIG``. Use the ``UPDATE_ON_FETCH`` and/or 26 | ``SHOW_TOOLBAR_CALLBACK`` settings instead. 27 | -------------------------------------------------------------------------------- /docs/commands.rst: -------------------------------------------------------------------------------- 1 | Commands 2 | ======== 3 | 4 | The Debug Toolbar currently provides one Django management command. 5 | 6 | ``debugsqlshell`` 7 | ----------------- 8 | 9 | This command starts an interactive Python shell, like Django's built-in 10 | ``shell`` management command. In addition, each ORM call that results in a 11 | database query will be beautifully output in the shell. 12 | 13 | Here's an example:: 14 | 15 | >>> from page.models import Page 16 | >>> ### Lookup and use resulting in an extra query... 17 | >>> p = Page.objects.get(pk=1) 18 | SELECT "page_page"."id", 19 | "page_page"."number", 20 | "page_page"."template_id", 21 | "page_page"."description" 22 | FROM "page_page" 23 | WHERE "page_page"."id" = 1 24 | 25 | >>> print(p.template.name) 26 | SELECT "page_template"."id", 27 | "page_template"."name", 28 | "page_template"."description" 29 | FROM "page_template" 30 | WHERE "page_template"."id" = 1 31 | 32 | Home 33 | >>> ### Using select_related to avoid 2nd database call... 34 | >>> p = Page.objects.select_related('template').get(pk=1) 35 | SELECT "page_page"."id", 36 | "page_page"."number", 37 | "page_page"."template_id", 38 | "page_page"."description", 39 | "page_template"."id", 40 | "page_template"."name", 41 | "page_template"."description" 42 | FROM "page_page" 43 | INNER JOIN "page_template" ON ("page_page"."template_id" = "page_template"."id") 44 | WHERE "page_page"."id" = 1 45 | 46 | >>> print(p.template.name) 47 | Home 48 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import datetime 14 | import os 15 | import sys 16 | 17 | os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings" 18 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "Django Debug Toolbar" 24 | copyright = "{}, Django Debug Toolbar developers and contributors" 25 | copyright = copyright.format(datetime.date.today().year) 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = "5.2.0" 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.intersphinx", 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = ["_build"] 48 | 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | 52 | # The theme to use for HTML and HTML Help pages. See the documentation for 53 | # a list of builtin themes. 54 | html_theme = "sphinx_rtd_theme" 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | # html_static_path = ['_static'] 60 | 61 | intersphinx_mapping = { 62 | "python": ("https://docs.python.org/", None), 63 | "django": ( 64 | "https://docs.djangoproject.com/en/dev/", 65 | "https://docs.djangoproject.com/en/dev/_objects/", 66 | ), 67 | } 68 | 69 | # -- Options for Read the Docs ----------------------------------------------- 70 | 71 | 72 | def setup(app): 73 | """Configure documentation via Sphinx extension""" 74 | # Add the :setting: role for intersphinx linking to Django's docs 75 | app.add_crossref_type( 76 | directivename="setting", 77 | rolename="setting", 78 | indextemplate="pair: %s; setting", 79 | ) 80 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django Debug Toolbar 2 | ==================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | installation 8 | configuration 9 | checks 10 | tips 11 | panels 12 | commands 13 | resources 14 | changes 15 | contributing 16 | architecture 17 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/resources.rst: -------------------------------------------------------------------------------- 1 | Resources 2 | ========= 3 | 4 | This section includes resources that can be used to learn more about 5 | the Django Debug Toolbar. 6 | 7 | Tutorials 8 | --------- 9 | 10 | Django Debugging Tutorial 11 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 12 | 13 | Originally presented as an in-person workshop at DjangoCon US 2022, this 14 | tutorial by **Tim Schilling** covers debugging techniques in Django. Follow 15 | along independently using the slides and GitHub repository. 16 | 17 | * `View the tutorial details on the conference website `__ 18 | * `Follow along with the GitHub repository `__ 19 | * `View the slides on Google Docs `__ 20 | * Last updated: February 13, 2025. 21 | * Estimated time to complete: 1-2 hours. 22 | 23 | Mastering Django Debug Toolbar: Efficient Debugging and Optimization Techniques 24 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 25 | 26 | This tutorial by **Bob Belderbos** provides an in-depth look at effectively 27 | using Django Debug Toolbar to debug Django applications, covering installation, 28 | configuration, and practical usage. 29 | 30 | * `Watch on YouTube `__ 31 | * Published: May 13, 2023. 32 | * Duration: 11 minutes. 33 | 34 | Talks 35 | ----- 36 | 37 | A Related Matter: Optimizing Your Web App by Using Django Debug Toolbar 38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 39 | 40 | Presented at DjangoCon US 2024 by **Christopher Adams**, this talk delves into 41 | optimizing web applications using Django Debug Toolbar, focusing on SQL query 42 | analysis and performance improvements. 43 | 44 | * `View the talk details on the conference website `__ 45 | * `Watch on DjangoTV `__ 46 | * Published: December 6, 2024. 47 | * Duration: 26 minutes. 48 | 49 | Fast on My Machine: How to Debug Slow Requests in Production 50 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 51 | 52 | Presented at DjangoCon Europe 2024 by **Raphael Michel**, this talk explores 53 | debugging slow requests in production. While not focused on Django Debug 54 | Toolbar, it highlights performance issues the tool can help diagnose. 55 | 56 | * `View the talk details on the conference website `__ 57 | * `Watch on DjangoTV `__ 58 | * Published: July 11, 2024. 59 | * Duration: 23 minutes. 60 | 61 | Want to Add Your Content Here? 62 | ------------------------------ 63 | 64 | Have a great tutorial or talk about Django Debug Toolbar? We'd love to 65 | showcase it! If your content helps developers improve their debugging skills, 66 | follow our :doc:`contributing guidelines ` to submit it. 67 | 68 | To ensure relevant and accessible content, please check the following 69 | before submitting: 70 | 71 | 1. Does it at least partially focus on the Django Debug Toolbar? 72 | 2. Does the content show a version of Django that is currently supported? 73 | 3. What language is the tutorial in and what languages are the captions 74 | available in? 75 | 76 | Talks and tutorials that cover advanced debugging techniques, 77 | performance optimization, and real-world applications are particularly 78 | welcome. 79 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | Hatchling 2 | Hotwire 3 | Jazzband 4 | Makefile 5 | Pympler 6 | Roboto 7 | Transifex 8 | Werkzeug 9 | aenable 10 | ajax 11 | asgi 12 | async 13 | backend 14 | backends 15 | backported 16 | biome 17 | checkbox 18 | contrib 19 | csp 20 | dicts 21 | django 22 | fallbacks 23 | flamegraph 24 | flatpages 25 | frontend 26 | htmx 27 | inlining 28 | instrumentation 29 | isort 30 | jQuery 31 | jinja 32 | jrestclient 33 | js 34 | margins 35 | memcache 36 | memcached 37 | middleware 38 | middlewares 39 | mixin 40 | mousedown 41 | mouseup 42 | multi 43 | neo 44 | nothreading 45 | paddings 46 | pre 47 | profiler 48 | psycopg 49 | py 50 | pyflame 51 | pylibmc 52 | pytest 53 | pyupgrade 54 | querysets 55 | refactoring 56 | reinitializing 57 | resizing 58 | runserver 59 | spellchecking 60 | spooler 61 | stacktrace 62 | stacktraces 63 | startup 64 | staticfiles 65 | theming 66 | timeline 67 | tox 68 | uWSGI 69 | unhandled 70 | unhashable 71 | validator 72 | -------------------------------------------------------------------------------- /docs/tips.rst: -------------------------------------------------------------------------------- 1 | Tips 2 | ==== 3 | 4 | The toolbar isn't displayed! 5 | ---------------------------- 6 | 7 | The Debug Toolbar will only display when ``DEBUG = True`` in your project's 8 | settings (see :ref:`Show Toolbar Callback `) and your 9 | IP address must also match an entry in your project's ``INTERNAL_IPS`` setting 10 | (see :ref:`internal-ips`). It will also only display if the MIME type of the 11 | response is either ``text/html`` or ``application/xhtml+xml`` and contains a 12 | closing ```` tag. 13 | 14 | Be aware of middleware ordering and other middleware that may intercept 15 | requests and return responses. Putting the debug toolbar middleware *after* the 16 | ``FlatpageFallbackMiddleware`` middleware, for example, means the toolbar will 17 | not show up on flatpages. 18 | 19 | Browsers have become more aggressive with caching static assets, such as 20 | JavaScript and CSS files. Check your browser's development console, and if 21 | you see errors, try a hard browser refresh or clearing your cache. 22 | 23 | Working with htmx and Turbo 24 | ---------------------------- 25 | 26 | Libraries such as `htmx `_ and 27 | `Turbo `_ need additional configuration to retain 28 | the toolbar handle element through page renders. This can be done by 29 | configuring the :ref:`ROOT_TAG_EXTRA_ATTRS ` to include 30 | the relevant JavaScript library's attribute. 31 | 32 | htmx 33 | ~~~~ 34 | 35 | The attribute `htmx `_ uses is 36 | `hx-preserve `_. 37 | 38 | Update your settings to include: 39 | 40 | .. code-block:: python 41 | 42 | DEBUG_TOOLBAR_CONFIG = { 43 | "ROOT_TAG_EXTRA_ATTRS": "hx-preserve" 44 | } 45 | 46 | Hotwire Turbo 47 | ~~~~~~~~~~~~~ 48 | 49 | The attribute `Turbo `_ uses is 50 | `data-turbo-permanent `_ 51 | 52 | Update your settings to include: 53 | 54 | .. code-block:: python 55 | 56 | DEBUG_TOOLBAR_CONFIG = { 57 | "ROOT_TAG_EXTRA_ATTRS": "data-turbo-permanent" 58 | } 59 | 60 | 61 | Performance considerations 62 | -------------------------- 63 | 64 | The Debug Toolbar is designed to introduce as little overhead as possible in 65 | the rendering of pages. However, depending on your project, the overhead may 66 | become noticeable. In extreme cases, it can make development impractical. 67 | Here's a breakdown of the performance issues you can run into and their 68 | solutions. 69 | 70 | Problems 71 | ~~~~~~~~ 72 | 73 | The Debug Toolbar works in two phases. First, it gathers data while Django 74 | handles a request and stores this data in memory. Second, when you open a 75 | panel in the browser, it fetches the data on the server and displays it. 76 | 77 | If you're seeing excessive CPU or memory consumption while browsing your site, 78 | you must optimize the "gathering" phase. If displaying a panel is slow, you 79 | must optimize the "rendering" phase. 80 | 81 | Culprits 82 | ~~~~~~~~ 83 | 84 | The SQL panel may be the culprit if your view performs many SQL queries. You 85 | should attempt to minimize the number of SQL queries, but this isn't always 86 | possible, for instance if you're using a CMS and have disabled caching for 87 | development. 88 | 89 | The cache panel is very similar to the SQL panel, except it isn't always a bad 90 | practice to make many cache queries in a view. 91 | 92 | The template panel becomes slow if your views or context processors return 93 | large contexts and your templates have complex inheritance or inclusion 94 | schemes. 95 | 96 | Solutions 97 | ~~~~~~~~~ 98 | 99 | If the "gathering" phase is too slow, you can disable problematic panels 100 | temporarily by deselecting the checkbox at the top right of each panel. That 101 | change will apply to the next request. If you don't use some panels at all, 102 | you can remove them permanently by customizing the ``DEBUG_TOOLBAR_PANELS`` 103 | setting. 104 | 105 | By default, data gathered during the last 25 requests is kept in memory. This 106 | allows you to use the toolbar on a page even if you have browsed to a few 107 | other pages since you first loaded that page. You can reduce memory 108 | consumption by setting the ``RESULTS_CACHE_SIZE`` configuration option to a 109 | lower value. At worst, the toolbar will tell you that the data you're looking 110 | for isn't available anymore. 111 | 112 | If the "rendering" phase is too slow, refrain from clicking on problematic 113 | panels :) Or reduce the amount of data gathered and rendered by these panels 114 | by disabling some configuration options that are enabled by default: 115 | 116 | - ``ENABLE_STACKTRACES`` for the SQL and cache panels, 117 | - ``SHOW_TEMPLATE_CONTEXT`` for the template panel. 118 | - ``PROFILER_CAPTURE_PROJECT_CODE`` and ``PROFILER_THRESHOLD_RATIO`` for the 119 | profiling panel. 120 | 121 | Also, check ``SKIP_TEMPLATE_PREFIXES`` when you're using template-based 122 | form widgets. 123 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | README 2 | ====== 3 | 4 | About 5 | ----- 6 | 7 | This sample project demonstrates how to use the debug toolbar. It is designed 8 | to run under the latest stable version of Django. 9 | 10 | It also provides a few test pages to ensure the debug toolbar doesn't 11 | interfere with common JavaScript frameworks. 12 | 13 | How to 14 | ------ 15 | 16 | The example project requires a working installation of Django and a few other 17 | packages:: 18 | 19 | $ python -m pip install -r requirements_dev.txt 20 | 21 | The following command must run from the root directory of Django Debug Toolbar, 22 | i.e. the directory that contains ``example/``:: 23 | 24 | $ make example 25 | 26 | This will create a database, superuser, and run the Django development server. 27 | The superuser's username will be the same as the current OS user and the 28 | password is "p". 29 | 30 | If you'd like to run these steps individually, use the following commands. 31 | Again, run from the root directory of Django Debug Toolbar. 32 | 33 | Create a database:: 34 | 35 | $ python example/manage.py migrate 36 | 37 | Create a superuser:: 38 | 39 | $ python example/manage.py createsuperuser 40 | 41 | Run the Django development server:: 42 | 43 | $ python example/manage.py runserver 44 | 45 | You can change the database used by specifying the ``DB_BACKEND`` 46 | environment variable:: 47 | 48 | $ DB_BACKEND=postgresql python example/manage.py migrate 49 | $ DB_BACKEND=postgresql python example/manage.py runserver 50 | 51 | Using an asynchronous (ASGI) server: 52 | 53 | Install [Daphne](https://pypi.org/project/daphne/) first: 54 | 55 | $ python -m pip install daphne 56 | 57 | Then run the Django development server: 58 | 59 | $ ASYNC_SERVER=true python example/manage.py runserver 60 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/example/__init__.py -------------------------------------------------------------------------------- /example/asgi.py: -------------------------------------------------------------------------------- 1 | """ASGI config for example project.""" 2 | 3 | import os 4 | 5 | from django.core.asgi import get_asgi_application 6 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 8 | 9 | application = get_asgi_application() 10 | -------------------------------------------------------------------------------- /example/django-debug-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/example/django-debug-toolbar.png -------------------------------------------------------------------------------- /example/example.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/example/example.db -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /example/screenshot.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import importlib 3 | import os 4 | import signal 5 | import subprocess 6 | from time import sleep 7 | 8 | from selenium.webdriver.common.by import By 9 | from selenium.webdriver.common.keys import Keys 10 | from selenium.webdriver.support import expected_conditions as EC 11 | from selenium.webdriver.support.wait import WebDriverWait 12 | 13 | 14 | def parse_args(): 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument("--browser", required=True) 17 | parser.add_argument("--headless", action="store_true") 18 | parser.add_argument("--outfile", "-o", required=True) 19 | parser.add_argument("--width", type=int, default=900) 20 | parser.add_argument("--height", type=int, default=700) 21 | return parser.parse_args() 22 | 23 | 24 | def create_webdriver_options(browser, headless): 25 | mod = importlib.import_module(f"selenium.webdriver.{browser}.options") 26 | options = mod.Options() 27 | if headless: 28 | options.headless = True 29 | return options 30 | 31 | 32 | def create_webdriver(browser, headless): 33 | mod = importlib.import_module(f"selenium.webdriver.{browser}.webdriver") 34 | return mod.WebDriver(options=create_webdriver_options(browser, headless)) 35 | 36 | 37 | def example_server(): 38 | proc = subprocess.Popen(["make", "example"]) 39 | # `make example` runs a few things before runserver. 40 | sleep(2) 41 | return proc 42 | 43 | 44 | def set_viewport_size(selenium, width, height): 45 | script = """ 46 | return [ 47 | window.outerWidth - window.innerWidth + arguments[0], 48 | window.outerHeight - window.innerHeight + arguments[1], 49 | ]; 50 | """ 51 | window_width, window_height = selenium.execute_script(script, width, height) 52 | selenium.set_window_size(window_width, window_height) 53 | 54 | 55 | def submit_form(selenium, data): 56 | url = selenium.current_url 57 | for name, value in data.items(): 58 | el = selenium.find_element(By.NAME, name) 59 | el.send_keys(value) 60 | el.send_keys(Keys.RETURN) 61 | WebDriverWait(selenium, timeout=5).until(EC.url_changes(url)) 62 | 63 | 64 | def main(): 65 | args = parse_args() 66 | with example_server() as p: 67 | try: 68 | with create_webdriver(args.browser, args.headless) as selenium: 69 | set_viewport_size(selenium, args.width, args.height) 70 | 71 | selenium.get("http://localhost:8000/admin/login/") 72 | submit_form(selenium, {"username": os.environ["USER"], "password": "p"}) 73 | 74 | selenium.get("http://localhost:8000/admin/auth/user/") 75 | # Check if SQL Panel is already visible: 76 | sql_panel = selenium.find_element(By.ID, "djdt-SQLPanel") 77 | if not sql_panel: 78 | # Open the admin sidebar. 79 | el = selenium.find_element(By.ID, "djDebugToolbarHandle") 80 | el.click() 81 | sql_panel = selenium.find_element(By.ID, "djdt-SQLPanel") 82 | # Open the SQL panel. 83 | sql_panel.click() 84 | 85 | selenium.save_screenshot(args.outfile) 86 | finally: 87 | p.send_signal(signal.SIGTERM) 88 | 89 | 90 | if __name__ == "__main__": 91 | main() 92 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | """Django settings for example project.""" 2 | 3 | import os 4 | import sys 5 | 6 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 7 | 8 | 9 | # Quick-start development settings - unsuitable for production 10 | 11 | 12 | SECRET_KEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" 13 | 14 | DEBUG = True 15 | 16 | INTERNAL_IPS = ["127.0.0.1", "::1"] 17 | 18 | # Application definition 19 | 20 | INSTALLED_APPS = [ 21 | *(["daphne"] if os.getenv("ASYNC_SERVER", False) else []), # noqa: FBT003 22 | "django.contrib.admin", 23 | "django.contrib.auth", 24 | "django.contrib.contenttypes", 25 | "django.contrib.sessions", 26 | "django.contrib.messages", 27 | "django.contrib.staticfiles", 28 | ] 29 | 30 | MIDDLEWARE = [ 31 | "django.middleware.security.SecurityMiddleware", 32 | "django.contrib.sessions.middleware.SessionMiddleware", 33 | "django.middleware.common.CommonMiddleware", 34 | "django.middleware.csrf.CsrfViewMiddleware", 35 | "django.contrib.auth.middleware.AuthenticationMiddleware", 36 | "django.contrib.messages.middleware.MessageMiddleware", 37 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 38 | ] 39 | 40 | ROOT_URLCONF = "example.urls" 41 | 42 | STATIC_URL = "/static/" 43 | 44 | TEMPLATES = [ 45 | { 46 | "NAME": "jinja2", 47 | "BACKEND": "django.template.backends.jinja2.Jinja2", 48 | "APP_DIRS": True, 49 | "DIRS": [os.path.join(BASE_DIR, "example", "templates", "jinja2")], 50 | }, 51 | { 52 | "BACKEND": "django.template.backends.django.DjangoTemplates", 53 | "APP_DIRS": True, 54 | "DIRS": [os.path.join(BASE_DIR, "example", "templates")], 55 | "OPTIONS": { 56 | "debug": True, 57 | "context_processors": [ 58 | "django.template.context_processors.debug", 59 | "django.template.context_processors.request", 60 | "django.contrib.auth.context_processors.auth", 61 | "django.contrib.messages.context_processors.messages", 62 | ], 63 | }, 64 | }, 65 | ] 66 | 67 | USE_TZ = True 68 | 69 | WSGI_APPLICATION = "example.wsgi.application" 70 | ASGI_APPLICATION = "example.asgi.application" 71 | 72 | 73 | # Cache and database 74 | 75 | CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}} 76 | 77 | DATABASES = { 78 | "default": { 79 | "ENGINE": "django.db.backends.sqlite3", 80 | "NAME": os.path.join(BASE_DIR, "example", "db.sqlite3"), 81 | } 82 | } 83 | 84 | # To use another database, set the DB_BACKEND environment variable. 85 | if os.environ.get("DB_BACKEND", "").lower() == "postgresql": 86 | # See docs/contributing for instructions on configuring PostgreSQL. 87 | DATABASES = { 88 | "default": { 89 | "ENGINE": "django.db.backends.postgresql", 90 | "NAME": "debug_toolbar", 91 | "USER": "debug_toolbar", 92 | "PASSWORD": "debug_toolbar", 93 | } 94 | } 95 | if os.environ.get("DB_BACKEND", "").lower() == "mysql": 96 | # See docs/contributing for instructions on configuring MySQL/MariaDB. 97 | DATABASES = { 98 | "default": { 99 | "ENGINE": "django.db.backends.mysql", 100 | "NAME": "debug_toolbar", 101 | "USER": "debug_toolbar", 102 | "PASSWORD": "debug_toolbar", 103 | } 104 | } 105 | 106 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "example", "static")] 107 | 108 | # Only enable the toolbar when we're in debug mode and we're 109 | # not running tests. Django will change DEBUG to be False for 110 | # tests, so we can't rely on DEBUG alone. 111 | ENABLE_DEBUG_TOOLBAR = DEBUG and "test" not in sys.argv 112 | if ENABLE_DEBUG_TOOLBAR: 113 | INSTALLED_APPS += [ 114 | "debug_toolbar", 115 | ] 116 | MIDDLEWARE += [ 117 | "debug_toolbar.middleware.DebugToolbarMiddleware", 118 | ] 119 | # Customize the config to support turbo and htmx boosting. 120 | DEBUG_TOOLBAR_CONFIG = {"ROOT_TAG_EXTRA_ATTRS": "data-turbo-permanent hx-preserve"} 121 | -------------------------------------------------------------------------------- /example/static/test.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /example/templates/async_db.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Async DB 6 | 7 | 8 |

    Async DB

    9 |

    10 | Value 11 | {{ user_count }} 12 |

    13 | 14 | 15 | -------------------------------------------------------------------------------- /example/templates/bad_form.html: -------------------------------------------------------------------------------- 1 | {% load cache %} 2 | 3 | 4 | 5 | 6 | Bad form 7 | 8 | 9 |

    Bad form test

    10 |
    11 | 12 |
    13 | 14 | 15 | -------------------------------------------------------------------------------- /example/templates/htmx/boost.html: -------------------------------------------------------------------------------- 1 | {% load cache %} 2 | 3 | 4 | 5 | 6 | Index of Tests (htmx) 7 | 8 | 9 | 10 |

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

    11 | 12 |

    13 | For the debug panel to remain through page navigation, add the setting: 14 |

    15 | DEBUG_TOOLBAR_CONFIG = {
    16 |   "ROOT_TAG_EXTRA_ATTRS": "hx-preserve"
    17 | }
    18 |       
    19 |

    20 | 21 | 26 | 27 | Home 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% load cache %} 2 | 3 | 4 | 5 | 6 | Index of Tests 7 | 8 | 9 |

    Index of Tests

    10 | {% cache 10 index_cache %} 11 | 20 |

    Django Admin

    21 | {% endcache %} 22 |

    23 | Value 24 | {{ request.session.value|default:0 }} 25 | 26 | 27 |

    28 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/templates/jinja2/index.jinja: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jinja Test 6 | 7 | 8 |

    jinja Test

    9 | {{ foo }} 10 | {% for i in range(10) %}{{ i }}{% endfor %} {# Jinja2 supports range(), Django templates do not #} 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/templates/jquery/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Test 6 | 7 | 13 | 14 | 15 |

    jQuery Test

    16 |

    If you see this, jQuery is working.

    17 | 18 | 19 | -------------------------------------------------------------------------------- /example/templates/mootools/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MooTools Test 6 | 7 | 13 | 14 | 15 |

    MooTools Test

    16 |

    If you see this, MooTools is working.

    17 | 18 | 19 | -------------------------------------------------------------------------------- /example/templates/prototype/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Prototype Test 6 | 7 | 13 | 14 | 15 |

    Prototype Test

    16 |

    If you see this, Prototype is working.

    17 | 18 | 19 | -------------------------------------------------------------------------------- /example/templates/turbo/index.html: -------------------------------------------------------------------------------- 1 | {% load cache %} 2 | 3 | 4 | 5 | 6 | Index of Tests 7 | 8 | 9 | 10 |

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

    11 | 12 |

    13 | For the debug panel to remain through page navigation, add the setting: 14 |

    15 | DEBUG_TOOLBAR_CONFIG = {
    16 |   "ROOT_TAG_EXTRA_ATTRS": "data-turbo-permanent"
    17 | }
    18 |       
    19 |

    20 | 25 | 26 |

    27 | Value 28 | {{ request.session.value|default:0 }} 29 | 30 | 31 |

    32 | 54 | Home 55 | 56 | 57 | -------------------------------------------------------------------------------- /example/test_views.py: -------------------------------------------------------------------------------- 1 | # Add tests to example app to check how the toolbar is used 2 | # when running tests for a project. 3 | # See https://github.com/django-commons/django-debug-toolbar/issues/1405 4 | 5 | from django.test import TestCase 6 | from django.urls import reverse 7 | 8 | 9 | class ViewTestCase(TestCase): 10 | def test_index(self): 11 | response = self.client.get(reverse("home")) 12 | assert response.status_code == 200 13 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from django.views.generic import TemplateView 4 | 5 | from debug_toolbar.toolbar import debug_toolbar_urls 6 | from example.views import ( 7 | async_db, 8 | async_db_concurrent, 9 | async_home, 10 | increment, 11 | jinja2_view, 12 | ) 13 | 14 | urlpatterns = [ 15 | path("", TemplateView.as_view(template_name="index.html"), name="home"), 16 | path( 17 | "bad-form/", 18 | TemplateView.as_view(template_name="bad_form.html"), 19 | name="bad_form", 20 | ), 21 | path("jinja/", jinja2_view, name="jinja"), 22 | path("async/", async_home, name="async_home"), 23 | path("async/db/", async_db, name="async_db"), 24 | path("async/db-concurrent/", async_db_concurrent, name="async_db_concurrent"), 25 | path("jquery/", TemplateView.as_view(template_name="jquery/index.html")), 26 | path("mootools/", TemplateView.as_view(template_name="mootools/index.html")), 27 | path("prototype/", TemplateView.as_view(template_name="prototype/index.html")), 28 | path( 29 | "htmx/boost/", 30 | TemplateView.as_view(template_name="htmx/boost.html"), 31 | name="htmx", 32 | ), 33 | path( 34 | "htmx/boost/2", 35 | TemplateView.as_view( 36 | template_name="htmx/boost.html", extra_context={"page_num": "2"} 37 | ), 38 | name="htmx2", 39 | ), 40 | path( 41 | "turbo/", TemplateView.as_view(template_name="turbo/index.html"), name="turbo" 42 | ), 43 | path( 44 | "turbo/2", 45 | TemplateView.as_view( 46 | template_name="turbo/index.html", extra_context={"page_num": "2"} 47 | ), 48 | name="turbo2", 49 | ), 50 | path("admin/", admin.site.urls), 51 | path("ajax/increment", increment, name="ajax_increment"), 52 | ] + debug_toolbar_urls() 53 | -------------------------------------------------------------------------------- /example/views.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asgiref.sync import sync_to_async 4 | from django.contrib.auth.models import User 5 | from django.http import JsonResponse 6 | from django.shortcuts import render 7 | 8 | 9 | def increment(request): 10 | try: 11 | value = int(request.session.get("value", 0)) + 1 12 | except ValueError: 13 | value = 1 14 | request.session["value"] = value 15 | return JsonResponse({"value": value}) 16 | 17 | 18 | def jinja2_view(request): 19 | return render(request, "index.jinja", {"foo": "bar"}, using="jinja2") 20 | 21 | 22 | async def async_home(request): 23 | return await sync_to_async(render)(request, "index.html") 24 | 25 | 26 | async def async_db(request): 27 | user_count = await User.objects.acount() 28 | 29 | return await sync_to_async(render)( 30 | request, "async_db.html", {"user_count": user_count} 31 | ) 32 | 33 | 34 | async def async_db_concurrent(request): 35 | # Do database queries concurrently 36 | (user_count, _) = await asyncio.gather( 37 | User.objects.acount(), User.objects.filter(username="test").acount() 38 | ) 39 | 40 | return await sync_to_async(render)( 41 | request, "async_db.html", {"user_count": user_count} 42 | ) 43 | -------------------------------------------------------------------------------- /example/wsgi.py: -------------------------------------------------------------------------------- 1 | """WSGI config for example project.""" 2 | 3 | import os 4 | 5 | from django.core.wsgi import get_wsgi_application 6 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 8 | 9 | application = get_wsgi_application() 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatchling", 5 | ] 6 | 7 | [project] 8 | name = "django-debug-toolbar" 9 | description = "A configurable set of panels that display various debug information about the current request/response." 10 | readme = "README.rst" 11 | license = { text = "BSD-3-Clause" } 12 | authors = [ 13 | { name = "Rob Hudson" }, 14 | ] 15 | requires-python = ">=3.9" 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Environment :: Web Environment", 19 | "Framework :: Django", 20 | "Framework :: Django :: 4.2", 21 | "Framework :: Django :: 5.0", 22 | "Framework :: Django :: 5.1", 23 | "Framework :: Django :: 5.2", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: BSD License", 26 | "Operating System :: OS Independent", 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: 3.13", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | ] 36 | dynamic = [ 37 | "version", 38 | ] 39 | dependencies = [ 40 | "django>=4.2.9", 41 | "sqlparse>=0.2", 42 | ] 43 | 44 | urls.Changelog = "https://django-debug-toolbar.readthedocs.io/en/latest/changes.html" 45 | urls.Documentation = "https://django-debug-toolbar.readthedocs.io/" 46 | urls.Download = "https://pypi.org/project/django-debug-toolbar/" 47 | urls.Homepage = "https://github.com/django-commons/django-debug-toolbar" 48 | urls.Issues = "https://github.com/django-commons/django-debug-toolbar/issues" 49 | urls.Source = "https://github.com/django-commons/django-debug-toolbar" 50 | 51 | [tool.hatch.build.targets.wheel] 52 | packages = [ 53 | "debug_toolbar", 54 | ] 55 | 56 | [tool.hatch.version] 57 | path = "debug_toolbar/__init__.py" 58 | 59 | [tool.ruff] 60 | target-version = "py39" 61 | 62 | fix = true 63 | show-fixes = true 64 | lint.extend-select = [ 65 | "ASYNC", # flake8-async 66 | "B", # flake8-bugbear 67 | "C4", # flake8-comprehensions 68 | "C90", # McCabe cyclomatic complexity 69 | "DJ", # flake8-django 70 | "E", # pycodestyle errors 71 | "F", # Pyflakes 72 | "FBT", # flake8-boolean-trap 73 | "I", # isort 74 | "INT", # flake8-gettext 75 | "PGH", # pygrep-hooks 76 | "PIE", # flake8-pie 77 | "RUF100", # Unused noqa directive 78 | "SLOT", # flake8-slots 79 | "UP", # pyupgrade 80 | "W", # pycodestyle warnings 81 | ] 82 | lint.extend-ignore = [ 83 | "B905", # Allow zip() without strict= 84 | "E501", # Ignore line length violations 85 | "UP031", # It's not always wrong to use percent-formatting 86 | ] 87 | lint.per-file-ignores."*/migrat*/*" = [ 88 | "N806", # Allow using PascalCase model names in migrations 89 | "N999", # Ignore the fact that migration files are invalid module names 90 | ] 91 | lint.isort.combine-as-imports = true 92 | lint.mccabe.max-complexity = 16 93 | 94 | [tool.coverage.html] 95 | skip_covered = true 96 | skip_empty = true 97 | 98 | [tool.coverage.run] 99 | branch = true 100 | parallel = true 101 | source = [ 102 | "debug_toolbar", 103 | ] 104 | 105 | [tool.coverage.paths] 106 | source = [ 107 | "src", 108 | ".tox/*/site-packages", 109 | ] 110 | 111 | [tool.coverage.report] 112 | # Update coverage badge link in README.rst when fail_under changes 113 | fail_under = 94 114 | show_missing = true 115 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | # Runtime dependencies 2 | 3 | Django 4 | sqlparse 5 | Jinja2 6 | 7 | # Testing 8 | 9 | coverage[toml] 10 | html5lib 11 | selenium 12 | tox 13 | black 14 | django-template-partials 15 | django-csp # Used in tests/test_csp_rendering 16 | 17 | # Integration support 18 | 19 | daphne # async in Example app 20 | 21 | # Documentation 22 | 23 | Sphinx 24 | sphinxcontrib-spelling 25 | sphinx-rtd-theme>1 26 | 27 | # Other tools 28 | 29 | pre-commit 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | sys.stderr.write( 6 | """\ 7 | =============================== 8 | Unsupported installation method 9 | =============================== 10 | This project no longer supports installation with `python setup.py install`. 11 | Please use `python -m pip install .` instead. 12 | """ 13 | ) 14 | sys.exit(1) 15 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/tests/__init__.py -------------------------------------------------------------------------------- /tests/additional_static/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import contextvars 2 | from typing import Optional 3 | 4 | import html5lib 5 | from asgiref.local import Local 6 | from django.http import HttpResponse 7 | from django.test import ( 8 | AsyncClient, 9 | AsyncRequestFactory, 10 | Client, 11 | RequestFactory, 12 | TestCase, 13 | TransactionTestCase, 14 | ) 15 | 16 | from debug_toolbar.panels import Panel 17 | from debug_toolbar.toolbar import DebugToolbar 18 | 19 | data_contextvar = contextvars.ContextVar("djdt_toolbar_test_client") 20 | 21 | 22 | class ToolbarTestClient(Client): 23 | def request(self, **request): 24 | # Use a thread/async task context-local variable to guard against a 25 | # concurrent _created signal from a different thread/task. 26 | data = Local() 27 | data.toolbar = None 28 | 29 | def handle_toolbar_created(sender, toolbar=None, **kwargs): 30 | data.toolbar = toolbar 31 | 32 | DebugToolbar._created.connect(handle_toolbar_created) 33 | try: 34 | response = super().request(**request) 35 | finally: 36 | DebugToolbar._created.disconnect(handle_toolbar_created) 37 | response.toolbar = data.toolbar 38 | 39 | return response 40 | 41 | 42 | class AsyncToolbarTestClient(AsyncClient): 43 | async def request(self, **request): 44 | # Use a thread/async task context-local variable to guard against a 45 | # concurrent _created signal from a different thread/task. 46 | # In cases testsuite will have both regular and async tests or 47 | # multiple async tests running in an eventloop making async_client calls. 48 | data_contextvar.set(None) 49 | 50 | def handle_toolbar_created(sender, toolbar=None, **kwargs): 51 | data_contextvar.set(toolbar) 52 | 53 | DebugToolbar._created.connect(handle_toolbar_created) 54 | try: 55 | response = await super().request(**request) 56 | finally: 57 | DebugToolbar._created.disconnect(handle_toolbar_created) 58 | response.toolbar = data_contextvar.get() 59 | 60 | return response 61 | 62 | 63 | rf = RequestFactory() 64 | arf = AsyncRequestFactory() 65 | 66 | 67 | class BaseMixin: 68 | _is_async = False 69 | client_class = ToolbarTestClient 70 | async_client_class = AsyncToolbarTestClient 71 | 72 | panel: Optional[Panel] = None 73 | panel_id = None 74 | 75 | def setUp(self): 76 | super().setUp() 77 | self._get_response = lambda request: HttpResponse() 78 | self.request = rf.get("/") 79 | if self._is_async: 80 | self.request = arf.get("/") 81 | self.toolbar = DebugToolbar(self.request, self.get_response_async) 82 | else: 83 | self.toolbar = DebugToolbar(self.request, self.get_response) 84 | self.toolbar.stats = {} 85 | 86 | if self.panel_id: 87 | self.panel = self.toolbar.get_panel_by_id(self.panel_id) 88 | self.panel.enable_instrumentation() 89 | else: 90 | self.panel = None 91 | 92 | def tearDown(self): 93 | if self.panel: 94 | self.panel.disable_instrumentation() 95 | super().tearDown() 96 | 97 | def get_response(self, request): 98 | return self._get_response(request) 99 | 100 | async def get_response_async(self, request): 101 | return self._get_response(request) 102 | 103 | def assertValidHTML(self, content): 104 | parser = html5lib.HTMLParser() 105 | parser.parseFragment(content) 106 | if parser.errors: 107 | msg_parts = ["Invalid HTML:"] 108 | lines = content.split("\n") 109 | for position, errorcode, datavars in parser.errors: 110 | msg_parts.append(f" {html5lib.constants.E[errorcode]}" % datavars) 111 | msg_parts.append(f" {lines[position[0] - 1]}") 112 | raise self.failureException("\n".join(msg_parts)) 113 | 114 | 115 | class BaseTestCase(BaseMixin, TestCase): 116 | pass 117 | 118 | 119 | class BaseMultiDBTestCase(BaseMixin, TransactionTestCase): 120 | databases = {"default", "replica"} 121 | 122 | 123 | class IntegrationTestCase(TestCase): 124 | """Base TestCase for tests involving clients making requests.""" 125 | 126 | def setUp(self): 127 | # The HistoryPanel keeps track of previous stores in memory. 128 | # This bleeds into other tests and violates their idempotency. 129 | # Clear the store before each test. 130 | for key in list(DebugToolbar._store.keys()): 131 | del DebugToolbar._store[key] 132 | super().setUp() 133 | -------------------------------------------------------------------------------- /tests/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/tests/commands/__init__.py -------------------------------------------------------------------------------- /tests/commands/test_debugsqlshell.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | 4 | from django.contrib.auth.models import User 5 | from django.core import management 6 | from django.db import connection 7 | from django.test import TestCase 8 | from django.test.utils import override_settings 9 | 10 | if connection.vendor == "postgresql": 11 | from django.db.backends.postgresql import base as base_module 12 | else: 13 | from django.db.backends import utils as base_module 14 | 15 | 16 | @override_settings(DEBUG=True) 17 | class DebugSQLShellTestCase(TestCase): 18 | def setUp(self): 19 | self.original_wrapper = base_module.CursorDebugWrapper 20 | # Since debugsqlshell monkey-patches django.db.backends.utils, we can 21 | # test it simply by loading it, without executing it. But we have to 22 | # undo the monkey-patch on exit. 23 | command_name = "debugsqlshell" 24 | app_name = management.get_commands()[command_name] 25 | management.load_command_class(app_name, command_name) 26 | 27 | def tearDown(self): 28 | base_module.CursorDebugWrapper = self.original_wrapper 29 | 30 | def test_command(self): 31 | original_stdout, sys.stdout = sys.stdout, io.StringIO() 32 | try: 33 | User.objects.count() 34 | self.assertIn("SELECT COUNT", sys.stdout.getvalue()) 35 | finally: 36 | sys.stdout = original_stdout 37 | -------------------------------------------------------------------------------- /tests/context_processors.py: -------------------------------------------------------------------------------- 1 | def broken(request): 2 | _read = request.non_existing_attribute 3 | -------------------------------------------------------------------------------- /tests/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class TemplateReprForm(forms.Form): 6 | user = forms.ModelChoiceField(queryset=User.objects.all()) 7 | 8 | def __repr__(self): 9 | return str(self) 10 | -------------------------------------------------------------------------------- /tests/loaders.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.template.loaders.app_directories import Loader 3 | 4 | 5 | class LoaderWithSQL(Loader): 6 | def get_template(self, *args, **kwargs): 7 | # Force the template loader to run some SQL. Simulates a CMS. 8 | User.objects.all().count() 9 | return super().get_template(*args, **kwargs) 10 | -------------------------------------------------------------------------------- /tests/middleware.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | 3 | 4 | class UseCacheAfterToolbar: 5 | """ 6 | This middleware exists to use the cache before and after 7 | the toolbar is setup. 8 | """ 9 | 10 | def __init__(self, get_response): 11 | self.get_response = get_response 12 | 13 | def __call__(self, request): 14 | cache.set("UseCacheAfterToolbar.before", 1) 15 | response = self.get_response(request) 16 | cache.set("UseCacheAfterToolbar.after", 1) 17 | return response 18 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.db.models import JSONField 4 | 5 | 6 | class NonAsciiRepr: 7 | def __repr__(self): 8 | return "nôt åscíì" 9 | 10 | 11 | class Binary(models.Model): 12 | field = models.BinaryField() 13 | 14 | def __str__(self): 15 | return "" 16 | 17 | 18 | class PostgresJSON(models.Model): 19 | field = JSONField() 20 | 21 | def __str__(self): 22 | return "" 23 | 24 | 25 | if settings.USE_GIS: 26 | from django.contrib.gis.db import models as gismodels 27 | 28 | class Location(gismodels.Model): 29 | point = gismodels.PointField() 30 | 31 | def __str__(self): 32 | return "" 33 | -------------------------------------------------------------------------------- /tests/panels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-debug-toolbar/c217334010fa54a8726640519ca29cb77fffda58/tests/panels/__init__.py -------------------------------------------------------------------------------- /tests/panels/test_alerts.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, StreamingHttpResponse 2 | from django.template import Context, Template 3 | 4 | from ..base import BaseTestCase 5 | 6 | 7 | class AlertsPanelTestCase(BaseTestCase): 8 | panel_id = "AlertsPanel" 9 | 10 | def test_alert_warning_display(self): 11 | """ 12 | Test that the panel (does not) display[s] an alert when there are 13 | (no) problems. 14 | """ 15 | self.panel.record_stats({"alerts": []}) 16 | self.assertNotIn("alerts", self.panel.nav_subtitle) 17 | 18 | self.panel.record_stats({"alerts": ["Alert 1", "Alert 2"]}) 19 | self.assertIn("2 alerts", self.panel.nav_subtitle) 20 | 21 | def test_file_form_without_enctype_multipart_form_data(self): 22 | """ 23 | Test that the panel displays a form invalid message when there is 24 | a file input but encoding not set to multipart/form-data. 25 | """ 26 | test_form = '
    ' 27 | result = self.panel.check_invalid_file_form_configuration(test_form) 28 | expected_error = ( 29 | 'Form with id "test-form" contains file input, ' 30 | 'but does not have the attribute enctype="multipart/form-data".' 31 | ) 32 | self.assertEqual(result[0]["alert"], expected_error) 33 | self.assertEqual(len(result), 1) 34 | 35 | def test_file_form_no_id_without_enctype_multipart_form_data(self): 36 | """ 37 | Test that the panel displays a form invalid message when there is 38 | a file input but encoding not set to multipart/form-data. 39 | 40 | This should use the message when the form has no id. 41 | """ 42 | test_form = '
    ' 43 | result = self.panel.check_invalid_file_form_configuration(test_form) 44 | expected_error = ( 45 | "Form contains file input, but does not have " 46 | 'the attribute enctype="multipart/form-data".' 47 | ) 48 | self.assertEqual(result[0]["alert"], expected_error) 49 | self.assertEqual(len(result), 1) 50 | 51 | def test_file_form_with_enctype_multipart_form_data(self): 52 | test_form = """
    53 | 54 |
    """ 55 | result = self.panel.check_invalid_file_form_configuration(test_form) 56 | 57 | self.assertEqual(len(result), 0) 58 | 59 | def test_file_form_with_enctype_multipart_form_data_in_button(self): 60 | test_form = """
    61 | 62 | 63 |
    """ 64 | result = self.panel.check_invalid_file_form_configuration(test_form) 65 | 66 | self.assertEqual(len(result), 0) 67 | 68 | def test_referenced_file_input_without_enctype_multipart_form_data(self): 69 | test_file_input = """
    70 | """ 71 | result = self.panel.check_invalid_file_form_configuration(test_file_input) 72 | 73 | expected_error = ( 74 | 'Input element references form with id "test-form", ' 75 | 'but the form does not have the attribute enctype="multipart/form-data".' 76 | ) 77 | self.assertEqual(result[0]["alert"], expected_error) 78 | self.assertEqual(len(result), 1) 79 | 80 | def test_referenced_file_input_with_enctype_multipart_form_data(self): 81 | test_file_input = """
    82 |
    83 | """ 84 | result = self.panel.check_invalid_file_form_configuration(test_file_input) 85 | 86 | self.assertEqual(len(result), 0) 87 | 88 | def test_integration_file_form_without_enctype_multipart_form_data(self): 89 | t = Template('
    ') 90 | c = Context({}) 91 | rendered_template = t.render(c) 92 | response = HttpResponse(content=rendered_template) 93 | 94 | self.panel.generate_stats(self.request, response) 95 | 96 | self.assertIn("1 alert", self.panel.nav_subtitle) 97 | self.assertIn( 98 | "Form with id "test-form" contains file input, " 99 | "but does not have the attribute enctype="multipart/form-data".", 100 | self.panel.content, 101 | ) 102 | 103 | def test_streaming_response(self): 104 | """Test to check for a streaming response.""" 105 | 106 | def _render(): 107 | yield "ok" 108 | 109 | response = StreamingHttpResponse(_render()) 110 | 111 | self.panel.generate_stats(self.request, response) 112 | self.assertEqual(self.panel.get_stats(), {}) 113 | -------------------------------------------------------------------------------- /tests/panels/test_async_panel_compatibility.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.test import AsyncRequestFactory, RequestFactory, TestCase 3 | 4 | from debug_toolbar.panels import Panel 5 | from debug_toolbar.toolbar import DebugToolbar 6 | 7 | 8 | class MockAsyncPanel(Panel): 9 | is_async = True 10 | 11 | 12 | class MockSyncPanel(Panel): 13 | is_async = False 14 | 15 | 16 | class PanelAsyncCompatibilityTestCase(TestCase): 17 | def setUp(self): 18 | self.async_factory = AsyncRequestFactory() 19 | self.wsgi_factory = RequestFactory() 20 | 21 | def test_panels_with_asgi(self): 22 | async_request = self.async_factory.get("/") 23 | toolbar = DebugToolbar(async_request, lambda request: HttpResponse()) 24 | 25 | async_panel = MockAsyncPanel(toolbar, async_request) 26 | sync_panel = MockSyncPanel(toolbar, async_request) 27 | 28 | self.assertTrue(async_panel.enabled) 29 | self.assertFalse(sync_panel.enabled) 30 | 31 | def test_panels_with_wsgi(self): 32 | wsgi_request = self.wsgi_factory.get("/") 33 | toolbar = DebugToolbar(wsgi_request, lambda request: HttpResponse()) 34 | 35 | async_panel = MockAsyncPanel(toolbar, wsgi_request) 36 | sync_panel = MockSyncPanel(toolbar, wsgi_request) 37 | 38 | self.assertTrue(async_panel.enabled) 39 | self.assertTrue(sync_panel.enabled) 40 | -------------------------------------------------------------------------------- /tests/panels/test_custom.py: -------------------------------------------------------------------------------- 1 | from django.test import override_settings 2 | 3 | from debug_toolbar.panels import Panel 4 | 5 | from ..base import IntegrationTestCase 6 | 7 | 8 | class CustomPanel(Panel): 9 | def title(self): 10 | return "Title with special chars &\"'<>" 11 | 12 | 13 | @override_settings( 14 | DEBUG=True, DEBUG_TOOLBAR_PANELS=["tests.panels.test_custom.CustomPanel"] 15 | ) 16 | class CustomPanelTestCase(IntegrationTestCase): 17 | def test_escapes_panel_title(self): 18 | response = self.client.get("/regular/basic/") 19 | self.assertContains( 20 | response, 21 | """ 22 |
  • 23 | 24 | 25 | Title with special chars &"'<> 26 | 27 |
  • 28 | """, 29 | html=True, 30 | ) 31 | self.assertContains( 32 | response, 33 | """ 34 |
    35 |
    36 |

    Title with special chars &"'<>

    37 | 38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 | """, 45 | html=True, 46 | ) 47 | -------------------------------------------------------------------------------- /tests/panels/test_profiling.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from django.contrib.auth.models import User 5 | from django.db import IntegrityError, transaction 6 | from django.http import HttpResponse 7 | from django.test.utils import override_settings 8 | 9 | from ..base import BaseTestCase, IntegrationTestCase 10 | from ..views import listcomp_view, regular_view 11 | 12 | 13 | @override_settings( 14 | DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.profiling.ProfilingPanel"] 15 | ) 16 | class ProfilingPanelTestCase(BaseTestCase): 17 | panel_id = "ProfilingPanel" 18 | 19 | def test_regular_view(self): 20 | self._get_response = lambda request: regular_view(request, "profiling") 21 | response = self.panel.process_request(self.request) 22 | self.panel.generate_stats(self.request, response) 23 | self.assertIn("func_list", self.panel.get_stats()) 24 | self.assertIn("regular_view", self.panel.content) 25 | 26 | def test_insert_content(self): 27 | """ 28 | Test that the panel only inserts content after generate_stats and 29 | not the process_request. 30 | """ 31 | self._get_response = lambda request: regular_view(request, "profiling") 32 | response = self.panel.process_request(self.request) 33 | # ensure the panel does not have content yet. 34 | self.assertNotIn("regular_view", self.panel.content) 35 | self.panel.generate_stats(self.request, response) 36 | # ensure the panel renders correctly. 37 | content = self.panel.content 38 | self.assertIn("regular_view", content) 39 | self.assertIn("render", content) 40 | self.assertValidHTML(content) 41 | 42 | @override_settings(DEBUG_TOOLBAR_CONFIG={"PROFILER_THRESHOLD_RATIO": 1}) 43 | def test_cum_time_threshold(self): 44 | """ 45 | Test that cumulative time threshold excludes calls 46 | """ 47 | self._get_response = lambda request: regular_view(request, "profiling") 48 | response = self.panel.process_request(self.request) 49 | self.panel.generate_stats(self.request, response) 50 | # ensure the panel renders but doesn't include our function. 51 | content = self.panel.content 52 | self.assertIn("regular_view", content) 53 | self.assertNotIn("render", content) 54 | self.assertValidHTML(content) 55 | 56 | @unittest.skipUnless( 57 | sys.version_info < (3, 12, 0), 58 | "Python 3.12 no longer contains a frame for list comprehensions.", 59 | ) 60 | def test_listcomp_escaped(self): 61 | self._get_response = lambda request: listcomp_view(request) 62 | response = self.panel.process_request(self.request) 63 | self.panel.generate_stats(self.request, response) 64 | content = self.panel.content 65 | self.assertNotIn('', content) 66 | self.assertIn('<listcomp>', content) 67 | 68 | def test_generate_stats_no_profiler(self): 69 | """ 70 | Test generating stats with no profiler. 71 | """ 72 | response = HttpResponse() 73 | self.assertIsNone(self.panel.generate_stats(self.request, response)) 74 | 75 | def test_generate_stats_no_root_func(self): 76 | """ 77 | Test generating stats using profiler without root function. 78 | """ 79 | response = self.panel.process_request(self.request) 80 | self.panel.profiler.clear() 81 | self.panel.profiler.enable() 82 | self.panel.profiler.disable() 83 | self.panel.generate_stats(self.request, response) 84 | self.assertNotIn("func_list", self.panel.get_stats()) 85 | 86 | 87 | @override_settings( 88 | DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.profiling.ProfilingPanel"] 89 | ) 90 | class ProfilingPanelIntegrationTestCase(IntegrationTestCase): 91 | def test_view_executed_once(self): 92 | self.assertEqual(User.objects.count(), 0) 93 | 94 | response = self.client.get("/new_user/") 95 | self.assertContains(response, "Profiling") 96 | self.assertEqual(User.objects.count(), 1) 97 | 98 | with self.assertRaises(IntegrityError), transaction.atomic(): 99 | response = self.client.get("/new_user/") 100 | self.assertEqual(User.objects.count(), 1) 101 | -------------------------------------------------------------------------------- /tests/panels/test_redirects.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from django.conf import settings 4 | from django.http import HttpResponse 5 | from django.test import AsyncRequestFactory 6 | 7 | from ..base import BaseTestCase 8 | 9 | 10 | class RedirectsPanelTestCase(BaseTestCase): 11 | panel_id = "RedirectsPanel" 12 | 13 | def test_regular_response(self): 14 | not_redirect = HttpResponse() 15 | self._get_response = lambda request: not_redirect 16 | response = self.panel.process_request(self.request) 17 | self.assertTrue(response is not_redirect) 18 | 19 | def test_not_a_redirect(self): 20 | redirect = HttpResponse(status=304) 21 | self._get_response = lambda request: redirect 22 | response = self.panel.process_request(self.request) 23 | self.assertTrue(response is redirect) 24 | 25 | def test_redirect(self): 26 | redirect = HttpResponse(status=302) 27 | redirect["Location"] = "http://somewhere/else/" 28 | self._get_response = lambda request: redirect 29 | response = self.panel.process_request(self.request) 30 | self.assertFalse(response is redirect) 31 | self.assertContains(response, "302 Found") 32 | self.assertContains(response, "http://somewhere/else/") 33 | 34 | def test_redirect_with_broken_context_processor(self): 35 | TEMPLATES = copy.deepcopy(settings.TEMPLATES) 36 | TEMPLATES[1]["OPTIONS"]["context_processors"] = [ 37 | "tests.context_processors.broken" 38 | ] 39 | 40 | with self.settings(TEMPLATES=TEMPLATES): 41 | redirect = HttpResponse(status=302) 42 | redirect["Location"] = "http://somewhere/else/" 43 | self._get_response = lambda request: redirect 44 | response = self.panel.process_request(self.request) 45 | self.assertFalse(response is redirect) 46 | self.assertContains(response, "302 Found") 47 | self.assertContains(response, "http://somewhere/else/") 48 | 49 | def test_unknown_status_code(self): 50 | redirect = HttpResponse(status=369) 51 | redirect["Location"] = "http://somewhere/else/" 52 | self._get_response = lambda request: redirect 53 | response = self.panel.process_request(self.request) 54 | self.assertContains(response, "369 Unknown Status Code") 55 | 56 | def test_unknown_status_code_with_reason(self): 57 | redirect = HttpResponse(status=369, reason="Look Ma!") 58 | redirect["Location"] = "http://somewhere/else/" 59 | self._get_response = lambda request: redirect 60 | response = self.panel.process_request(self.request) 61 | self.assertContains(response, "369 Look Ma!") 62 | 63 | def test_insert_content(self): 64 | """ 65 | Test that the panel only inserts content after generate_stats and 66 | not the process_request. 67 | """ 68 | redirect = HttpResponse(status=304) 69 | self._get_response = lambda request: redirect 70 | response = self.panel.process_request(self.request) 71 | self.assertIsNotNone(response) 72 | response = self.panel.generate_stats(self.request, redirect) 73 | self.assertIsNone(response) 74 | 75 | async def test_async_compatibility(self): 76 | redirect = HttpResponse(status=302) 77 | 78 | async def get_response(request): 79 | return redirect 80 | 81 | await_response = await get_response(self.request) 82 | self._get_response = get_response 83 | 84 | self.request = AsyncRequestFactory().get("/") 85 | response = await self.panel.process_request(self.request) 86 | self.assertIsInstance(response, HttpResponse) 87 | self.assertTrue(response is await_response) 88 | 89 | def test_original_response_preserved(self): 90 | redirect = HttpResponse(status=302) 91 | redirect["Location"] = "http://somewhere/else/" 92 | self._get_response = lambda request: redirect 93 | response = self.panel.process_request(self.request) 94 | self.assertFalse(response is redirect) 95 | self.assertTrue(hasattr(response, "original_response")) 96 | self.assertTrue(response.original_response is redirect) 97 | self.assertIsNone(response.get("Location")) 98 | self.assertEqual( 99 | response.original_response.get("Location"), "http://somewhere/else/" 100 | ) 101 | -------------------------------------------------------------------------------- /tests/panels/test_settings.py: -------------------------------------------------------------------------------- 1 | from django.test import override_settings 2 | 3 | from ..base import IntegrationTestCase 4 | 5 | 6 | @override_settings(DEBUG=True) 7 | class SettingsIntegrationTestCase(IntegrationTestCase): 8 | def test_panel_title(self): 9 | response = self.client.get("/regular/basic/") 10 | # The settings module is None due to using Django's UserSettingsHolder 11 | # in tests. 12 | self.assertContains( 13 | response, 14 | """ 15 |
  • 16 | 17 | Settings 18 |
  • 19 | """, 20 | html=True, 21 | ) 22 | self.assertContains( 23 | response, 24 | """ 25 |
    26 |
    27 |

    Settings from None

    28 | 29 |
    30 |
    31 |
    32 |
    33 |
    34 |
    35 | """, 36 | html=True, 37 | ) 38 | -------------------------------------------------------------------------------- /tests/panels/test_versions.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from ..base import BaseTestCase 4 | 5 | version_info_t = namedtuple( 6 | "version_info_t", ("major", "minor", "micro", "releaselevel", "serial") 7 | ) 8 | 9 | 10 | class VersionsPanelTestCase(BaseTestCase): 11 | panel_id = "VersionsPanel" 12 | 13 | def test_app_version_from_get_version_fn(self): 14 | class FakeApp: 15 | def get_version(self): 16 | return version_info_t(1, 2, 3, "", "") 17 | 18 | self.assertEqual(self.panel.get_app_version(FakeApp()), "1.2.3") 19 | 20 | def test_incompatible_app_version_fn(self): 21 | class FakeApp: 22 | def get_version(self, some_other_arg): 23 | # This should be ignored by the get_version_from_app 24 | return version_info_t(0, 0, 0, "", "") 25 | 26 | VERSION = version_info_t(1, 2, 3, "", "") 27 | 28 | self.assertEqual(self.panel.get_app_version(FakeApp()), "1.2.3") 29 | 30 | def test_app_version_from_VERSION(self): 31 | class FakeApp: 32 | VERSION = version_info_t(1, 2, 3, "", "") 33 | 34 | self.assertEqual(self.panel.get_app_version(FakeApp()), "1.2.3") 35 | 36 | def test_app_version_from_underscore_version(self): 37 | class FakeApp: 38 | __version__ = version_info_t(1, 2, 3, "", "") 39 | 40 | self.assertEqual(self.panel.get_app_version(FakeApp()), "1.2.3") 41 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """Django settings for tests.""" 2 | 3 | import os 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 6 | 7 | 8 | # Quick-start development settings - unsuitable for production 9 | 10 | DEBUG = False 11 | SECRET_KEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" 12 | 13 | INTERNAL_IPS = ["127.0.0.1"] 14 | 15 | LOGGING = { # avoids spurious output in tests 16 | "version": 1, 17 | "disable_existing_loggers": True, 18 | } 19 | 20 | 21 | # Application definition 22 | 23 | INSTALLED_APPS = [ 24 | "django.contrib.admin", 25 | "django.contrib.auth", 26 | "django.contrib.contenttypes", 27 | "django.contrib.sessions", 28 | "django.contrib.messages", 29 | "django.contrib.staticfiles", 30 | "debug_toolbar", 31 | # We are not actively using template-partials; we just want more nesting 32 | # in our template loader configuration, see 33 | # https://github.com/django-commons/django-debug-toolbar/issues/2109 34 | "template_partials", 35 | "tests", 36 | ] 37 | 38 | 39 | USE_GIS = os.getenv("DB_BACKEND") == "postgis" 40 | 41 | if USE_GIS: 42 | INSTALLED_APPS = ["django.contrib.gis"] + INSTALLED_APPS 43 | 44 | MEDIA_URL = "/media/" # Avoids https://code.djangoproject.com/ticket/21451 45 | 46 | MIDDLEWARE = [ 47 | "tests.middleware.UseCacheAfterToolbar", 48 | "debug_toolbar.middleware.DebugToolbarMiddleware", 49 | "django.middleware.security.SecurityMiddleware", 50 | "django.contrib.sessions.middleware.SessionMiddleware", 51 | "django.middleware.common.CommonMiddleware", 52 | "django.middleware.csrf.CsrfViewMiddleware", 53 | "django.contrib.auth.middleware.AuthenticationMiddleware", 54 | "django.contrib.messages.middleware.MessageMiddleware", 55 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 56 | ] 57 | 58 | ROOT_URLCONF = "tests.urls" 59 | 60 | TEMPLATES = [ 61 | { 62 | "NAME": "jinja2", 63 | "BACKEND": "django.template.backends.jinja2.Jinja2", 64 | "APP_DIRS": True, 65 | "DIRS": [os.path.join(BASE_DIR, "tests", "templates", "jinja2")], 66 | }, 67 | { 68 | "BACKEND": "django.template.backends.django.DjangoTemplates", 69 | "APP_DIRS": True, 70 | "OPTIONS": { 71 | "context_processors": [ 72 | "django.template.context_processors.debug", 73 | "django.template.context_processors.request", 74 | "django.contrib.auth.context_processors.auth", 75 | "django.contrib.messages.context_processors.messages", 76 | ] 77 | }, 78 | }, 79 | ] 80 | 81 | USE_TZ = True 82 | 83 | STATIC_ROOT = os.path.join(BASE_DIR, "tests", "static") 84 | 85 | STATIC_URL = "/static/" 86 | 87 | STATICFILES_DIRS = [ 88 | os.path.join(BASE_DIR, "tests", "additional_static"), 89 | ("prefix", os.path.join(BASE_DIR, "tests", "additional_static")), 90 | ] 91 | 92 | # Cache and database 93 | 94 | CACHES = { 95 | "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, 96 | "second": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, 97 | } 98 | 99 | DATABASES = { 100 | "default": { 101 | "ENGINE": "django.{}db.backends.{}".format( 102 | "contrib.gis." if USE_GIS else "", os.getenv("DB_BACKEND", "sqlite3") 103 | ), 104 | "NAME": os.getenv("DB_NAME", ":memory:"), 105 | "USER": os.getenv("DB_USER"), 106 | "PASSWORD": os.getenv("DB_PASSWORD"), 107 | "HOST": os.getenv("DB_HOST", ""), 108 | "PORT": os.getenv("DB_PORT", ""), 109 | "TEST": { 110 | "USER": "default_test", 111 | }, 112 | }, 113 | "replica": { 114 | "ENGINE": "django.{}db.backends.{}".format( 115 | "contrib.gis." if USE_GIS else "", os.getenv("DB_BACKEND", "sqlite3") 116 | ), 117 | "NAME": os.getenv("DB_NAME", ":memory:"), 118 | "USER": os.getenv("DB_USER"), 119 | "PASSWORD": os.getenv("DB_PASSWORD"), 120 | "HOST": os.getenv("DB_HOST", ""), 121 | "PORT": os.getenv("DB_PORT", ""), 122 | "TEST": { 123 | "USER": "default_test", 124 | "MIRROR": "default", 125 | }, 126 | }, 127 | } 128 | 129 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 130 | 131 | # Debug Toolbar configuration 132 | 133 | DEBUG_TOOLBAR_CONFIG = { 134 | # Django's test client sets wsgi.multiprocess to True inappropriately 135 | "RENDER_PANELS": False, 136 | # IS_RUNNING_TESTS must be False even though we're running tests because we're running the toolbar's own tests. 137 | "IS_RUNNING_TESTS": False, 138 | } 139 | -------------------------------------------------------------------------------- /tests/sync.py: -------------------------------------------------------------------------------- 1 | """ 2 | Taken from channels.db 3 | """ 4 | 5 | from asgiref.sync import SyncToAsync 6 | from django.db import close_old_connections 7 | 8 | 9 | class DatabaseSyncToAsync(SyncToAsync): 10 | """ 11 | SyncToAsync version that cleans up old database connections when it exits. 12 | """ 13 | 14 | def thread_handler(self, loop, *args, **kwargs): 15 | close_old_connections() 16 | try: 17 | return super().thread_handler(loop, *args, **kwargs) 18 | finally: 19 | close_old_connections() 20 | 21 | 22 | # The class is TitleCased, but we want to encourage use as a callable/decorator 23 | database_sync_to_async = DatabaseSyncToAsync 24 | -------------------------------------------------------------------------------- /tests/templates/ajax/ajax.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
    click for ajax
    5 | 6 | 22 | {% endblock content %} 23 | -------------------------------------------------------------------------------- /tests/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | {% block head %}{% endblock %} 6 | 7 | 8 | {% block content %}{% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/templates/basic.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %}Test for {{ title }}{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/templates/jinja2/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | {% block content %}{% endblock %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/templates/jinja2/basic.jinja: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | Test for {{ title }} (Jinja) 5 | {% for i in range(10) %}{{ i }}{% endfor %} {# Jinja2 supports range(), Django templates do not #} 6 | {% endblock content %} 7 | -------------------------------------------------------------------------------- /tests/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | -------------------------------------------------------------------------------- /tests/templates/sql/flat.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {{ users }} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /tests/templates/sql/included.html: -------------------------------------------------------------------------------- 1 | {{ users }} 2 | -------------------------------------------------------------------------------- /tests/templates/sql/nested.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include "sql/included.html" %} 5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /tests/templates/staticfiles/async_static.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block head %} 5 | 6 | {% endblock head %} 7 | -------------------------------------------------------------------------------- /tests/templates/staticfiles/path.html: -------------------------------------------------------------------------------- 1 | {% load static %}{% static path %} 2 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.http import Http404, HttpResponse 4 | from django.test import AsyncRequestFactory, RequestFactory, TestCase 5 | from django.test.utils import override_settings 6 | 7 | from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar 8 | 9 | 10 | @render_with_toolbar_language 11 | def stub_view(request): 12 | return HttpResponse(200) 13 | 14 | 15 | @require_show_toolbar 16 | def stub_require_toolbar_view(request): 17 | return HttpResponse(200) 18 | 19 | 20 | @require_show_toolbar 21 | async def stub_require_toolbar_async_view(request): 22 | return HttpResponse(200) 23 | 24 | 25 | class TestRequireToolbar(TestCase): 26 | """ 27 | Tests require_toolbar functionality and async compatibility. 28 | """ 29 | 30 | def setUp(self): 31 | self.factory = RequestFactory() 32 | self.async_factory = AsyncRequestFactory() 33 | 34 | @override_settings(DEBUG=True) 35 | def test_require_toolbar_debug_true(self): 36 | response = stub_require_toolbar_view(self.factory.get("/")) 37 | self.assertEqual(response.status_code, 200) 38 | 39 | def test_require_toolbar_debug_false(self): 40 | with self.assertRaises(Http404): 41 | stub_require_toolbar_view(self.factory.get("/")) 42 | 43 | # Following tests additionally tests async compatibility 44 | # of require_toolbar decorator 45 | @override_settings(DEBUG=True) 46 | async def test_require_toolbar_async_debug_true(self): 47 | response = await stub_require_toolbar_async_view(self.async_factory.get("/")) 48 | self.assertEqual(response.status_code, 200) 49 | 50 | async def test_require_toolbar_async_debug_false(self): 51 | with self.assertRaises(Http404): 52 | await stub_require_toolbar_async_view(self.async_factory.get("/")) 53 | 54 | 55 | @override_settings(DEBUG=True, LANGUAGE_CODE="fr") 56 | class RenderWithToolbarLanguageTestCase(TestCase): 57 | @override_settings(DEBUG_TOOLBAR_CONFIG={"TOOLBAR_LANGUAGE": "de"}) 58 | @patch("debug_toolbar.decorators.language_override") 59 | def test_uses_toolbar_language(self, mock_language_override): 60 | stub_view(RequestFactory().get("/")) 61 | mock_language_override.assert_called_once_with("de") 62 | 63 | @patch("debug_toolbar.decorators.language_override") 64 | def test_defaults_to_django_language_code(self, mock_language_override): 65 | stub_view(RequestFactory().get("/")) 66 | mock_language_override.assert_called_once_with("fr") 67 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | from django import forms 4 | from django.test import TestCase 5 | 6 | from debug_toolbar.forms import SignedDataForm 7 | 8 | SIGNATURE = "-WiogJKyy4E8Om00CrFSy0T6XHObwBa6Zb46u-vmeYE" 9 | 10 | DATA = {"date": datetime(2020, 1, 1, tzinfo=timezone.utc), "value": "foo"} 11 | SIGNED_DATA = f'{{"date": "2020-01-01 00:00:00+00:00", "value": "foo"}}:{SIGNATURE}' 12 | 13 | 14 | class FooForm(forms.Form): 15 | value = forms.CharField() 16 | # Include a datetime in the tests because it's not serializable back 17 | # to a datetime by SignedDataForm 18 | date = forms.DateTimeField() 19 | 20 | 21 | class TestSignedDataForm(TestCase): 22 | def test_signed_data(self): 23 | data = {"signed": SignedDataForm.sign(DATA)} 24 | form = SignedDataForm(data=data) 25 | self.assertTrue(form.is_valid()) 26 | # Check the signature value 27 | self.assertEqual(data["signed"], SIGNED_DATA) 28 | 29 | def test_verified_data(self): 30 | form = SignedDataForm(data={"signed": SignedDataForm.sign(DATA)}) 31 | self.assertEqual( 32 | form.verified_data(), 33 | { 34 | "value": "foo", 35 | "date": "2020-01-01 00:00:00+00:00", 36 | }, 37 | ) 38 | # Take it back to the foo form to validate the datetime is serialized 39 | foo_form = FooForm(data=form.verified_data()) 40 | self.assertTrue(foo_form.is_valid()) 41 | self.assertDictEqual(foo_form.cleaned_data, DATA) 42 | 43 | def test_initial_set_signed(self): 44 | form = SignedDataForm(initial=DATA) 45 | self.assertEqual(form.initial["signed"], SIGNED_DATA) 46 | 47 | def test_prevents_tampering(self): 48 | data = {"signed": SIGNED_DATA.replace('"value": "foo"', '"value": "bar"')} 49 | form = SignedDataForm(data=data) 50 | self.assertFalse(form.is_valid()) 51 | -------------------------------------------------------------------------------- /tests/test_login_not_required.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import django 4 | from django.test import SimpleTestCase, override_settings 5 | from django.urls import reverse 6 | 7 | 8 | @unittest.skipIf( 9 | django.VERSION < (5, 1), 10 | "Valid on Django 5.1 and above, requires LoginRequiredMiddleware", 11 | ) 12 | @override_settings( 13 | DEBUG=True, 14 | MIDDLEWARE=[ 15 | "django.contrib.sessions.middleware.SessionMiddleware", 16 | "django.contrib.auth.middleware.AuthenticationMiddleware", 17 | "django.contrib.auth.middleware.LoginRequiredMiddleware", 18 | "debug_toolbar.middleware.DebugToolbarMiddleware", 19 | ], 20 | ) 21 | class LoginNotRequiredTestCase(SimpleTestCase): 22 | def test_panels(self): 23 | for uri in ( 24 | "history_sidebar", 25 | "history_refresh", 26 | "sql_select", 27 | "sql_explain", 28 | "sql_profile", 29 | "template_source", 30 | ): 31 | with self.subTest(uri=uri): 32 | response = self.client.get(reverse(f"djdt:{uri}")) 33 | self.assertNotEqual(response.status_code, 200) 34 | 35 | def test_render_panel(self): 36 | response = self.client.get( 37 | reverse("djdt:render_panel"), query_params={"store_id": "store_id"} 38 | ) 39 | self.assertEqual(response.status_code, 200) 40 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest.mock import patch 3 | 4 | from django.contrib.auth.models import User 5 | from django.http import HttpResponse 6 | from django.test import AsyncRequestFactory, RequestFactory, TestCase, override_settings 7 | 8 | from debug_toolbar.middleware import DebugToolbarMiddleware 9 | 10 | 11 | def show_toolbar_if_staff(request): 12 | # Hit the database, but always return True 13 | return User.objects.exists() or True 14 | 15 | 16 | async def ashow_toolbar_if_staff(request): 17 | # Hit the database, but always return True 18 | has_users = await User.objects.afirst() 19 | return has_users or True 20 | 21 | 22 | class MiddlewareSyncAsyncCompatibilityTestCase(TestCase): 23 | def setUp(self): 24 | self.factory = RequestFactory() 25 | self.async_factory = AsyncRequestFactory() 26 | 27 | @override_settings(DEBUG=True) 28 | def test_sync_mode(self): 29 | """ 30 | test middleware switches to sync (__call__) based on get_response type 31 | """ 32 | 33 | request = self.factory.get("/") 34 | middleware = DebugToolbarMiddleware( 35 | lambda x: HttpResponse("Test app") 36 | ) 37 | 38 | self.assertFalse(asyncio.iscoroutinefunction(middleware)) 39 | 40 | response = middleware(request) 41 | self.assertEqual(response.status_code, 200) 42 | self.assertIn(b"djdt", response.content) 43 | 44 | @override_settings(DEBUG=True) 45 | async def test_async_mode(self): 46 | """ 47 | test middleware switches to async (__acall__) based on get_response type 48 | and returns a coroutine 49 | """ 50 | 51 | async def get_response(request): 52 | return HttpResponse("Test app") 53 | 54 | middleware = DebugToolbarMiddleware(get_response) 55 | request = self.async_factory.get("/") 56 | 57 | self.assertTrue(asyncio.iscoroutinefunction(middleware)) 58 | 59 | response = await middleware(request) 60 | self.assertEqual(response.status_code, 200) 61 | self.assertIn(b"djdt", response.content) 62 | 63 | @override_settings(DEBUG=True) 64 | @patch( 65 | "debug_toolbar.middleware.show_toolbar_func_or_path", 66 | return_value=ashow_toolbar_if_staff, 67 | ) 68 | def test_async_show_toolbar_callback_sync_middleware(self, mocked_show): 69 | def get_response(request): 70 | return HttpResponse("Hello world") 71 | 72 | middleware = DebugToolbarMiddleware(get_response) 73 | 74 | request = self.factory.get("/") 75 | response = middleware(request) 76 | self.assertEqual(response.status_code, 200) 77 | self.assertIn(b"djdt", response.content) 78 | 79 | @override_settings(DEBUG=True) 80 | @patch( 81 | "debug_toolbar.middleware.show_toolbar_func_or_path", 82 | return_value=show_toolbar_if_staff, 83 | ) 84 | async def test_sync_show_toolbar_callback_async_middleware(self, mocked_show): 85 | async def get_response(request): 86 | return HttpResponse("Hello world") 87 | 88 | middleware = DebugToolbarMiddleware(get_response) 89 | 90 | request = self.async_factory.get("/") 91 | response = await middleware(request) 92 | self.assertEqual(response.status_code, 200) 93 | self.assertIn(b"djdt", response.content) 94 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.test import TestCase 4 | 5 | from debug_toolbar.settings import _is_running_tests 6 | 7 | 8 | class SettingsTestCase(TestCase): 9 | @patch("debug_toolbar.settings.sys") 10 | @patch("debug_toolbar.settings.os") 11 | def test_is_running_tests(self, mock_os, mock_sys): 12 | mock_sys.argv = "test" 13 | mock_os.environ = {} 14 | self.assertTrue(_is_running_tests()) 15 | 16 | mock_sys.argv = "" 17 | mock_os.environ = {} 18 | self.assertFalse(_is_running_tests()) 19 | 20 | mock_sys.argv = "" 21 | mock_os.environ = {"PYTEST_VERSION": "1"} 22 | self.assertTrue(_is_running_tests()) 23 | -------------------------------------------------------------------------------- /tests/test_toolbar.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | 3 | from debug_toolbar.toolbar import debug_toolbar_urls 4 | from tests.base import BaseTestCase 5 | 6 | 7 | class DebugToolbarUrlsTestCase(BaseTestCase): 8 | def test_empty_prefix_errors(self): 9 | with self.assertRaises(ImproperlyConfigured): 10 | debug_toolbar_urls(prefix="") 11 | 12 | def test_empty_when_debug_is_false(self): 13 | self.assertEqual(debug_toolbar_urls(), []) 14 | 15 | def test_has_path(self): 16 | with self.settings(DEBUG=True): 17 | self.assertEqual(len(debug_toolbar_urls()), 1) 18 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.views import LoginView 3 | from django.urls import include, path, re_path 4 | 5 | from . import views 6 | from .models import NonAsciiRepr 7 | 8 | urlpatterns = [ 9 | re_path( 10 | r"^resolving1/(.+)/(.+)/$", views.resolving_view, name="positional-resolving" 11 | ), 12 | path("resolving2///", views.resolving_view), 13 | re_path(r"^resolving3/(.+)/$", views.resolving_view, {"arg2": "default"}), 14 | re_path(r"^regular/(?P.*)/$", views.regular_view), 15 | re_path(r"^template_response/(?P<title>.*)/$", views.template_response_view), 16 | re_path(r"^regular_jinja/(?P<title>.*)/$", views.regular_jinjia_view), 17 | path("non_ascii_request/", views.regular_view, {"title": NonAsciiRepr()}), 18 | path("new_user/", views.new_user), 19 | path("execute_sql/", views.execute_sql), 20 | path("async_execute_sql/", views.async_execute_sql), 21 | path("async_execute_sql_concurrently/", views.async_execute_sql_concurrently), 22 | path("cached_view/", views.cached_view), 23 | path("cached_low_level_view/", views.cached_low_level_view), 24 | path("json_view/", views.json_view), 25 | path("redirect/", views.redirect_view), 26 | path("ajax/", views.ajax_view), 27 | path("login_without_redirect/", LoginView.as_view(redirect_field_name=None)), 28 | path("csp_view/", views.csp_view), 29 | path("admin/", admin.site.urls), 30 | path("__debug__/", include("debug_toolbar.urls")), 31 | ] 32 | -------------------------------------------------------------------------------- /tests/urls_invalid.py: -------------------------------------------------------------------------------- 1 | """Invalid urls.py file for testing""" 2 | 3 | urlpatterns = [] 4 | -------------------------------------------------------------------------------- /tests/urls_use_package_urls.py: -------------------------------------------------------------------------------- 1 | """urls.py to test using debug_toolbar.urls in include""" 2 | 3 | from django.urls import include, path 4 | 5 | import debug_toolbar 6 | 7 | from . import views 8 | 9 | urlpatterns = [ 10 | path("cached_view/", views.cached_view), 11 | path("__debug__/", include(debug_toolbar.urls)), 12 | ] 13 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asgiref.sync import sync_to_async 4 | from django.contrib.auth.models import User 5 | from django.core.cache import cache 6 | from django.http import HttpResponseRedirect, JsonResponse 7 | from django.shortcuts import render 8 | from django.template.response import TemplateResponse 9 | from django.views.decorators.cache import cache_page 10 | 11 | 12 | def execute_sql(request): 13 | list(User.objects.all()) 14 | return render(request, "base.html") 15 | 16 | 17 | async def async_execute_sql(request): 18 | """ 19 | Some query API can be executed asynchronously but some requires 20 | async version of itself. 21 | 22 | https://docs.djangoproject.com/en/5.1/topics/db/queries/#asynchronous-queries 23 | """ 24 | list_store = [] 25 | 26 | # make async query with filter, which is compatible with async for. 27 | async for user in User.objects.filter(username="test"): 28 | list_store.append(user) 29 | 30 | # make async query with afirst 31 | async_fetched_user = await User.objects.filter(username="test").afirst() 32 | list_store.append(async_fetched_user) 33 | return render(request, "base.html") 34 | 35 | 36 | async def async_execute_sql_concurrently(request): 37 | await asyncio.gather(sync_to_async(list)(User.objects.all()), User.objects.acount()) 38 | return render(request, "base.html") 39 | 40 | 41 | def regular_view(request, title): 42 | return render(request, "basic.html", {"title": title}) 43 | 44 | 45 | def csp_view(request): 46 | """Use request.csp_nonce to inject it into the headers""" 47 | return render(request, "basic.html", {"title": f"CSP {request.csp_nonce}"}) 48 | 49 | 50 | def template_response_view(request, title): 51 | return TemplateResponse(request, "basic.html", {"title": title}) 52 | 53 | 54 | def new_user(request, username="joe"): 55 | User.objects.create_user(username=username) 56 | return render(request, "basic.html", {"title": "new user"}) 57 | 58 | 59 | def resolving_view(request, arg1, arg2): 60 | # see test_url_resolving in tests.py 61 | return render(request, "base.html") 62 | 63 | 64 | @cache_page(60) 65 | def cached_view(request): 66 | return render(request, "base.html") 67 | 68 | 69 | def cached_low_level_view(request): 70 | key = "spam" 71 | value = cache.get(key) 72 | if not value: 73 | value = "eggs" 74 | cache.set(key, value, 60) 75 | return render(request, "base.html") 76 | 77 | 78 | def json_view(request): 79 | return JsonResponse({"foo": "bar"}) 80 | 81 | 82 | def regular_jinjia_view(request, title): 83 | return render(request, "basic.jinja", {"title": title}, using="jinja2") 84 | 85 | 86 | def listcomp_view(request): 87 | lst = [i for i in range(50000) if i % 2 == 0] 88 | return render(request, "basic.html", {"title": "List comprehension", "lst": lst}) 89 | 90 | 91 | def redirect_view(request): 92 | return HttpResponseRedirect("/regular/redirect/") 93 | 94 | 95 | def ajax_view(request): 96 | return render(request, "ajax/ajax.html") 97 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | envlist = 4 | docs 5 | packaging 6 | py{39,310,311,312}-dj{42}-{sqlite,postgresql,postgis,mysql} 7 | py{310,311,312}-dj{42,50,51,52}-{sqlite,postgresql,psycopg3,postgis,mysql} 8 | py{313}-dj{51,52,main}-{sqlite,psycopg3,postgis3,mysql} 9 | 10 | [testenv] 11 | deps = 12 | dj42: django~=4.2.1 13 | dj50: django~=5.0.2 14 | dj51: django~=5.1.0 15 | dj52: django~=5.2.0a1 16 | djmain: https://github.com/django/django/archive/main.tar.gz 17 | postgresql: psycopg2-binary 18 | psycopg3: psycopg[binary] 19 | postgis: psycopg2-binary 20 | postgis3: psycopg[binary] 21 | mysql: mysqlclient 22 | coverage[toml] 23 | Jinja2 24 | html5lib 25 | pygments 26 | selenium>=4.8.0 27 | sqlparse 28 | django-csp 29 | django-template-partials 30 | passenv= 31 | CI 32 | COVERAGE_ARGS 33 | DB_BACKEND 34 | DB_NAME 35 | DB_USER 36 | DB_PASSWORD 37 | DB_HOST 38 | DB_PORT 39 | DISPLAY 40 | DJANGO_SELENIUM_TESTS 41 | GITHUB_* 42 | setenv = 43 | PYTHONPATH = {toxinidir} 44 | PYTHONWARNINGS = d 45 | py311-dj42-postgresql: DJANGO_SELENIUM_TESTS = {env:DJANGO_SELENIUM_TESTS:true} 46 | DB_NAME = {env:DB_NAME:debug_toolbar} 47 | DB_USER = {env:DB_USER:debug_toolbar} 48 | DB_HOST = {env:DB_HOST:localhost} 49 | DB_PASSWORD = {env:DB_PASSWORD:debug_toolbar} 50 | DJANGO_SETTINGS_MODULE = tests.settings 51 | allowlist_externals = make 52 | pip_pre = True 53 | commands = python -b -W always -m coverage run -m django test -v2 {posargs:tests} 54 | 55 | 56 | [testenv:py{39,310,311,312,313}-dj{42,50,51,52,main}-{postgresql,psycopg3}] 57 | setenv = 58 | {[testenv]setenv} 59 | DB_BACKEND = postgresql 60 | DB_PORT = {env:DB_PORT:5432} 61 | 62 | 63 | [testenv:py{39,310,311,312,313}-dj{42,50,51,52,main}-{postgis,postgis3}] 64 | setenv = 65 | {[testenv]setenv} 66 | DB_BACKEND = postgis 67 | DB_PORT = {env:DB_PORT:5432} 68 | 69 | 70 | [testenv:py{39,310,311,312,313}-dj{42,50,51,52,main}-mysql] 71 | setenv = 72 | {[testenv]setenv} 73 | DB_BACKEND = mysql 74 | DB_PORT = {env:DB_PORT:3306} 75 | 76 | 77 | [testenv:py{39,310,311,312,313}-dj{42,50,51,52,main}-sqlite] 78 | setenv = 79 | {[testenv]setenv} 80 | DB_BACKEND = sqlite3 81 | DB_NAME = ":memory:" 82 | 83 | 84 | [testenv:docs] 85 | commands = make -C {toxinidir}/docs {posargs:spelling} 86 | deps = 87 | Sphinx 88 | sphinxcontrib-spelling 89 | sphinx-rtd-theme 90 | 91 | [testenv:packaging] 92 | commands = 93 | python -m build 94 | twine check --strict dist/* 95 | deps = 96 | build 97 | twine 98 | skip_install = true 99 | 100 | [gh-actions] 101 | python = 102 | 3.9: py39 103 | 3.10: py310 104 | 3.11: py311 105 | 3.12: py312 106 | 3.13: py313 107 | 108 | [gh-actions:env] 109 | DB_BACKEND = 110 | mysql: mysql 111 | postgresql: postgresql 112 | psycopg3: psycopg3 113 | postgis: postgis 114 | postgis3: postgis3 115 | sqlite3: sqlite 116 | --------------------------------------------------------------------------------