├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── ddt_request_history ├── __init__.py └── panels │ ├── __init__.py │ ├── request_history.html │ └── request_history.py ├── setup.cfg └── setup.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '41 9 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/ 3 | build/ 4 | dist/ 5 | django_debug_toolbar_request_history.egg-info/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, David Sutherland 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | recursive-include ddt_request_history *.py *.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Update: History is now built into Django Debug Toolbar # 2 | 3 | Since version [3.0](https://django-debug-toolbar.readthedocs.io/en/latest/changes.html#) of Django Debug Toolbar a built in history panel is available. More details can be found in this [PR](https://github.com/jazzband/django-debug-toolbar/pull/1250) and the [docs](https://django-debug-toolbar.readthedocs.io/en/latest/panels.html#history). As this functionality is being provided by the toolbar itself there will likely not be any further updates to this project (though if there are good reasons for an exception like supporting older versions of the toolbar it will be considered). Thanks to all those who've contributed to and used this project. 4 | 5 | ## Request History Panel for Django Debug Toolbar ## 6 | 7 | Adds a request history panel to [Django Debug Toolbar](https://github.com/django-debug-toolbar/django-debug-toolbar) for viewing stats for different requests (with the option for ajax support). 8 | 9 | 10 | ### Install ### 11 | 12 | ```bash 13 | pip install django-debug-toolbar-request-history 14 | ``` 15 | 16 | Then add the panel to ```DEBUG_TOOLBAR_PANELS``` (see the config section for more details). 17 | 18 | **Note: only Django Debug Toolbar versions 2.0 and higher are now supported. For older versions try:** 19 | 20 | ```bash 21 | pip install django-debug-toolbar-request-history==0.0.11 22 | ``` 23 | 24 | or for the development version: 25 | 26 | ```bash 27 | pip install -e git+https://github.com/djsutho/django-debug-toolbar-request-history.git#egg=django-debug-toolbar-request-history 28 | ``` 29 | 30 | ### Usage ### 31 | 32 | * Click on the "Request History" panel in the toolbar to load the available requests 33 | * Click on the request you are interested in (on the "Time" or "Path" part of the request) to load the toolbar for that request 34 | 35 | 36 | **Notes** 37 | 38 | Due to django-debug-toolbar reliance on thread-local: 39 | - currently requests do not survive server reload, therefore, when using the dev server old requests will not be available after a code change is loaded 40 | - if you get inconsistent request history each time you click on the panel, lower your server threads to 1 41 | 42 | 43 | ### Config (in settings.py) ### 44 | 45 | To ```DEBUG_TOOLBAR_PANELS``` add ```'ddt_request_history.panels.request_history.RequestHistoryPanel'``` e.g.: 46 | 47 | ```python 48 | DEBUG_TOOLBAR_PANELS = [ 49 | 'ddt_request_history.panels.request_history.RequestHistoryPanel', # Here it is 50 | 'debug_toolbar.panels.versions.VersionsPanel', 51 | 'debug_toolbar.panels.timer.TimerPanel', 52 | 'debug_toolbar.panels.settings.SettingsPanel', 53 | 'debug_toolbar.panels.headers.HeadersPanel', 54 | 'debug_toolbar.panels.request.RequestPanel', 55 | 'debug_toolbar.panels.sql.SQLPanel', 56 | 'debug_toolbar.panels.templates.TemplatesPanel', 57 | 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 58 | 'debug_toolbar.panels.cache.CachePanel', 59 | 'debug_toolbar.panels.signals.SignalsPanel', 60 | 'debug_toolbar.panels.logging.LoggingPanel', 61 | 'debug_toolbar.panels.redirects.RedirectsPanel', 62 | 'debug_toolbar.panels.profiling.ProfilingPanel', 63 | ] 64 | ``` 65 | 66 | To change the number of stored requests add ```RESULTS_STORE_SIZE``` to ```DEBUG_TOOLBAR_CONFIG``` e.g.: 67 | 68 | ```python 69 | DEBUG_TOOLBAR_CONFIG = { 70 | 'RESULTS_STORE_SIZE': 100, 71 | } 72 | ``` 73 | 74 | ### TODO ### 75 | * Clean-up 76 | * Change the storage to survive server reloads (maybe use cache or session). 77 | * Add tests 78 | -------------------------------------------------------------------------------- /ddt_request_history/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsutho/django-debug-toolbar-request-history/df23cf96f7106453bce087397834aa6e1927621e/ddt_request_history/__init__.py -------------------------------------------------------------------------------- /ddt_request_history/panels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djsutho/django-debug-toolbar-request-history/df23cf96f7106453bce087397834aa6e1927621e/ddt_request_history/panels/__init__.py -------------------------------------------------------------------------------- /ddt_request_history/panels/request_history.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% load static %} 2 |
3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for id, toolbar in toolbars.items %} 17 | 18 | 19 | 22 | 25 | 28 | 36 | 37 | {% endfor %} 38 | 39 |
#{% trans "Time" %}{% trans "Method" %}{% trans "Path" %}{% trans "Post Variables" %}
{{forloop.counter}} 20 | {{ toolbar.toolbar.stats.RequestHistoryPanel.time|escape }} 21 | 23 |

{{ toolbar.toolbar.stats.RequestHistoryPanel.request_method|escape }}

24 |
26 |

{{ toolbar.toolbar.stats.RequestHistoryPanel.request_url|escape }}

27 |
29 | + 30 | {% if trunc_length %} 31 | {{ toolbar.toolbar.stats.RequestHistoryPanel.post|truncatechars:trunc_length|escape }} 32 | {% else %} 33 | {{ toolbar.toolbar.stats.RequestHistoryPanel.post|escape }} 34 | {% endif %} 35 |
40 |
41 | 42 | 113 | 114 | 115 | 123 |
-------------------------------------------------------------------------------- /ddt_request_history/panels/request_history.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | import logging 5 | import os 6 | import re 7 | import sys 8 | import threading 9 | import uuid 10 | 11 | import debug_toolbar 12 | 13 | from collections import OrderedDict 14 | from datetime import datetime 15 | from distutils.version import LooseVersion 16 | 17 | from django.conf import settings 18 | from django.template import Template 19 | from django.template.backends.django import DjangoTemplates 20 | from django.template.context import Context 21 | from django.utils.translation import gettext_lazy as _ 22 | 23 | from debug_toolbar.panels import Panel 24 | from debug_toolbar.settings import get_config 25 | from debug_toolbar.toolbar import DebugToolbar 26 | 27 | try: 28 | from collections.abc import Callable 29 | except ImportError: # Python < 3.3 30 | from collections import Callable 31 | 32 | try: 33 | toolbar_version = LooseVersion(debug_toolbar.VERSION) 34 | except: 35 | toolbar_version = LooseVersion('0') 36 | 37 | logger = logging.getLogger(__name__) 38 | 39 | DEBUG_TOOLBAR_URL_PREFIX = getattr(settings, 'DEBUG_TOOLBAR_URL_PREFIX', '/__debug__') 40 | 41 | _original_middleware_call = None 42 | 43 | def patched_middleware_call(self, request): 44 | # Decide whether the toolbar is active for this request. 45 | show_toolbar = debug_toolbar.middleware.get_show_toolbar() 46 | if not show_toolbar(request): 47 | return self.get_response(request) 48 | 49 | toolbar = DebugToolbar(request, self.get_response) 50 | 51 | # Activate instrumentation ie. monkey-patch. 52 | for panel in toolbar.enabled_panels: 53 | panel.enable_instrumentation() 54 | try: 55 | # Run panels like Django middleware. 56 | response = toolbar.process_request(request) 57 | finally: 58 | # Deactivate instrumentation ie. monkey-unpatch. This must run 59 | # regardless of the response. Keep 'return' clauses below. 60 | for panel in reversed(toolbar.enabled_panels): 61 | panel.disable_instrumentation() 62 | # When the toolbar will be inserted for sure, generate the stats. 63 | for panel in reversed(toolbar.enabled_panels): 64 | panel.generate_stats(request, response) 65 | panel.generate_server_timing(request, response) 66 | 67 | response = self.generate_server_timing_header( 68 | response, toolbar.enabled_panels 69 | ) 70 | 71 | # Check for responses where the toolbar can't be inserted. 72 | content_encoding = response.get("Content-Encoding", "") 73 | content_type = response.get("Content-Type", "").split(";")[0] 74 | if any( 75 | ( 76 | getattr(response, "streaming", False), 77 | "gzip" in content_encoding, 78 | content_type not in debug_toolbar.middleware._HTML_TYPES, 79 | ) 80 | ): 81 | return response 82 | 83 | # Collapse the toolbar by default if SHOW_COLLAPSED is set. 84 | if toolbar.config["SHOW_COLLAPSED"] and "djdt" not in request.COOKIES: 85 | response.set_cookie("djdt", "hide", 864000) 86 | 87 | # Insert the toolbar in the response. 88 | content = response.content.decode(response.charset) 89 | insert_before = get_config()["INSERT_BEFORE"] 90 | pattern = re.escape(insert_before) 91 | bits = re.split(pattern, content, flags=re.IGNORECASE) 92 | if len(bits) > 1: 93 | 94 | bits[-2] += toolbar.render_toolbar() 95 | response.content = insert_before.join(bits) 96 | if response.get("Content-Length", None): 97 | response["Content-Length"] = len(response.content) 98 | return response 99 | 100 | 101 | def patch_middleware(): 102 | if not this_module.middleware_patched: 103 | try: 104 | from debug_toolbar.middleware import DebugToolbarMiddleware 105 | this_module._original_middleware_call = DebugToolbarMiddleware.__call__ 106 | DebugToolbarMiddleware.__call__ = patched_middleware_call 107 | except ImportError: 108 | return 109 | this_module.middleware_patched = True 110 | 111 | 112 | middleware_patched = False 113 | template = None 114 | this_module = sys.modules[__name__] 115 | 116 | # XXX: need to call this as early as possible but we have circular imports when 117 | # running with gunicorn so also try a second later 118 | patch_middleware() 119 | threading.Timer(1.0, patch_middleware, ()).start() 120 | 121 | 122 | def get_template(): 123 | if this_module.template is None: 124 | template_path = os.path.join( 125 | os.path.dirname(os.path.realpath(__file__)), 126 | 'request_history.html' 127 | ) 128 | with open(template_path) as template_file: 129 | this_module.template = Template( 130 | template_file.read(), 131 | engine=DjangoTemplates({'NAME': 'rh', 'DIRS': [], 'APP_DIRS': False, 'OPTIONS': {}}).engine 132 | ) 133 | return this_module.template 134 | 135 | 136 | def allow_ajax(request): 137 | """ 138 | Default function to determine whether to show the toolbar on a given page. 139 | """ 140 | if request.META.get('REMOTE_ADDR', None) not in settings.INTERNAL_IPS: 141 | return False 142 | return bool(settings.DEBUG) 143 | 144 | 145 | def patched_store(self): 146 | if self.store_id: # don't save if already have 147 | return 148 | self.store_id = uuid.uuid4().hex 149 | cls = type(self) 150 | cls._store[self.store_id] = self 151 | store_size = get_config().get('RESULTS_CACHE_SIZE', get_config().get('RESULTS_STORE_SIZE', 100)) 152 | for dummy in range(len(cls._store) - store_size): 153 | try: 154 | # collections.OrderedDict 155 | cls._store.popitem(last=False) 156 | except TypeError: 157 | # django.utils.datastructures.SortedDict 158 | del cls._store[cls._store.keyOrder[0]] 159 | 160 | 161 | def patched_fetch(cls, store_id): 162 | return cls._store.get(store_id) 163 | 164 | 165 | DebugToolbar.store = patched_store 166 | DebugToolbar.fetch = classmethod(patched_fetch) 167 | 168 | 169 | class RequestHistoryPanel(Panel): 170 | """ A panel to display Request History """ 171 | 172 | title = _("Request History") 173 | 174 | template = 'request_history.html' 175 | 176 | @property 177 | def nav_subtitle(self): 178 | return self.get_stats().get('request_url', '') 179 | 180 | def generate_stats(self, request, response): 181 | self.record_stats({ 182 | 'request_url': request.get_full_path(), 183 | 'request_method': request.method, 184 | 'post': json.dumps(request.POST, sort_keys=True, indent=4), 185 | 'time': datetime.now(), 186 | }) 187 | 188 | def process_request(self, request): 189 | self.record_stats({ 190 | 'request_url': request.get_full_path(), 191 | 'request_method': request.method, 192 | 'post': json.dumps(request.POST, sort_keys=True, indent=4), 193 | 'time': datetime.now(), 194 | }) 195 | return super().process_request(request) 196 | 197 | @property 198 | def content(self): 199 | """ Content of the panel when it's displayed in full screen. """ 200 | toolbars = OrderedDict() 201 | for id, toolbar in DebugToolbar._store.items(): 202 | content = {} 203 | for panel in toolbar.panels: 204 | panel_id = None 205 | nav_title = '' 206 | nav_subtitle = '' 207 | try: 208 | panel_id = panel.panel_id 209 | nav_title = panel.nav_title 210 | nav_subtitle = panel.nav_subtitle() if isinstance( 211 | panel.nav_subtitle, Callable) else panel.nav_subtitle 212 | except Exception: 213 | logger.debug('Error parsing panel info:', exc_info=True) 214 | if panel_id is not None: 215 | content.update({ 216 | panel_id: { 217 | 'panel_id': panel_id, 218 | 'nav_title': nav_title, 219 | 'nav_subtitle': nav_subtitle, 220 | } 221 | }) 222 | toolbars[id] = { 223 | 'toolbar': toolbar, 224 | 'content': content 225 | } 226 | return get_template().render(Context({ 227 | 'toolbars': OrderedDict(reversed(list(toolbars.items()))), 228 | 'trunc_length': get_config().get('RH_POST_TRUNC_LENGTH', 0) 229 | })) 230 | 231 | def disable_instrumentation(self): 232 | request_panel = self.toolbar.stats.get(self.panel_id) 233 | if request_panel and not request_panel.get('request_url', '').startswith(DEBUG_TOOLBAR_URL_PREFIX): 234 | self.toolbar.store() 235 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | long_description = file: README.md 3 | long_description_content_type = text/markdown 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='django-debug-toolbar-request-history', 5 | version='0.1.4', 6 | description='Request History Panel for Django Debug Toolbar', 7 | author='David Sutherland', 8 | author_email='djsutho@gmail.com', 9 | license='BSD', 10 | license_files=['LICENSE'], 11 | packages=find_packages(exclude=('tests', 'example')), 12 | include_package_data=True, 13 | install_requires=['django-debug-toolbar>=2.0'], 14 | url='https://github.com/djsutho/django-debug-toolbar-request-history', 15 | download_url='https://github.com/djsutho/django-debug-toolbar-request-history/tarball/0.1.4', 16 | keywords=['django', 'debug', 'ajax'], 17 | classifiers=[ 18 | 'Development Status :: 5 - Production/Stable', 19 | 'Environment :: Web Environment', 20 | 'Framework :: Django', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 3', 26 | "Programming Language :: Python :: 3 :: Only", 27 | 'Programming Language :: Python :: 3.4', 28 | 'Programming Language :: Python :: 3.5', 29 | 'Programming Language :: Python :: 3.6', 30 | 'Programming Language :: Python :: 3.7', 31 | 'Programming Language :: Python :: 3.8', 32 | 'Topic :: Software Development :: Libraries :: Python Modules', 33 | ], 34 | ) 35 | --------------------------------------------------------------------------------