Skip to content

Commit 3568de8

Browse files
committed
Add HistoryPanel to capture ajax requests.
This creates a new panel, HistoryPanel which makes use of the Toolbar.store to support a history of the toolbar as requests are made. The interface changes as follows: Panel.is_historical - Indicates that the panel's button and content should be updated when switching between snapshots of the history. Toolbar.store - Will no longer generate a new store_id when the instance already has a value. DEBUG_TOOLBAR_CONFIG.HISTORY_POST_TRUNC_LENGTH - Allows the request's POST content to be truncated in the history panel's content. LoggingPanel and StaticFilesPanel now utilize the ``get_stats`` method to fetch panel data for nav_subtitle. Credit to @djsutho for creating the original third party panel: https://github.com/djsutho/django-debug-toolbar-request-history The core concepts were derived from that package.
1 parent 080ce84 commit 3568de8

23 files changed

+446
-66
lines changed

debug_toolbar/middleware.py

+10-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import re
66
from functools import lru_cache
77

8-
import django
98
from django.conf import settings
109
from django.utils.module_loading import import_string
1110

@@ -43,14 +42,9 @@ def __init__(self, get_response):
4342
self.get_response = get_response
4443

4544
def __call__(self, request):
46-
# Decide whether the toolbar is active for this request. Don't render
47-
# the toolbar during AJAX requests.
45+
# Decide whether the toolbar is active for this request.
4846
show_toolbar = get_show_toolbar()
49-
if not show_toolbar(request) or (
50-
request.is_ajax()
51-
if django.VERSION < (3, 1)
52-
else not request.accepts("text/html")
53-
):
47+
if not show_toolbar(request):
5448
return self.get_response(request)
5549

5650
toolbar = DebugToolbar(request, self.get_response)
@@ -67,6 +61,14 @@ def __call__(self, request):
6761
for panel in reversed(toolbar.enabled_panels):
6862
panel.disable_instrumentation()
6963

64+
# Generate the stats for all requests when the toolbar is being shown,
65+
# but not necessarily inserted.
66+
for panel in reversed(toolbar.enabled_panels):
67+
panel.generate_stats(request, response)
68+
panel.generate_server_timing(request, response)
69+
70+
response = self.generate_server_timing_header(response, toolbar.enabled_panels)
71+
7072
# Check for responses where the toolbar can't be inserted.
7173
content_encoding = response.get("Content-Encoding", "")
7274
content_type = response.get("Content-Type", "").split(";")[0]
@@ -85,15 +87,6 @@ def __call__(self, request):
8587
pattern = re.escape(insert_before)
8688
bits = re.split(pattern, content, flags=re.IGNORECASE)
8789
if len(bits) > 1:
88-
# When the toolbar will be inserted for sure, generate the stats.
89-
for panel in reversed(toolbar.enabled_panels):
90-
panel.generate_stats(request, response)
91-
panel.generate_server_timing(request, response)
92-
93-
response = self.generate_server_timing_header(
94-
response, toolbar.enabled_panels
95-
)
96-
9790
bits[-2] += toolbar.render_toolbar()
9891
response.content = insert_before.join(bits)
9992
if "Content-Length" in response:

debug_toolbar/panels/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ def has_content(self):
6363
"""
6464
return True
6565

66+
@property
67+
def is_historical(self):
68+
"""
69+
Panel supports rendering historical values.
70+
71+
Defaults to :attr:`has_content`.
72+
"""
73+
return self.has_content
74+
6675
@property
6776
def title(self):
6877
"""
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from debug_toolbar.panels.history.panel import HistoryPanel # noqa

debug_toolbar/panels/history/forms.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import hashlib
2+
import hmac
3+
4+
from django import forms
5+
from django.conf import settings
6+
from django.core.exceptions import ValidationError
7+
from django.utils.crypto import constant_time_compare
8+
from django.utils.encoding import force_bytes
9+
10+
11+
class HistoryStoreForm(forms.Form):
12+
"""
13+
Validate params
14+
15+
store_id: The key for the store instance to be fetched.
16+
"""
17+
18+
store_id = forms.CharField(widget=forms.HiddenInput())
19+
hash = forms.CharField(widget=forms.HiddenInput())
20+
21+
def __init__(self, *args, **kwargs):
22+
initial = kwargs.get("initial", None)
23+
24+
if initial is not None:
25+
initial["hash"] = self.make_hash(initial)
26+
27+
super().__init__(*args, **kwargs)
28+
29+
def make_hash(self, data):
30+
m = hmac.new(key=force_bytes(settings.SECRET_KEY), digestmod=hashlib.sha1)
31+
m.update(force_bytes(data["store_id"]))
32+
return m.hexdigest()
33+
34+
def clean_hash(self):
35+
hash = self.cleaned_data["hash"]
36+
37+
if not constant_time_compare(hash, self.make_hash(self.data)):
38+
raise ValidationError("Tamper alert")
39+
40+
return hash

debug_toolbar/panels/history/panel.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import json
2+
import logging
3+
from collections import OrderedDict
4+
5+
from django.conf.urls import url
6+
from django.template.loader import render_to_string
7+
from django.utils import timezone
8+
from django.utils.translation import gettext_lazy as _
9+
10+
from debug_toolbar import settings as dt_settings
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+
logger = logging.getLogger(__name__)
16+
17+
CLEANSED_SUBSTITUTE = "********************"
18+
19+
20+
class HistoryPanel(Panel):
21+
""" A panel to display History """
22+
23+
title = _("History")
24+
nav_title = _("History")
25+
template = "debug_toolbar/panels/history.html"
26+
27+
@property
28+
def is_historical(self):
29+
"""The HistoryPanel should not be included in the historical panels."""
30+
return False
31+
32+
@classmethod
33+
def get_urls(cls):
34+
return [
35+
url(r"^history_sidebar/$", views.history_sidebar, name="history_sidebar"),
36+
]
37+
38+
@property
39+
def nav_subtitle(self):
40+
return self.get_stats().get("request_url", "")
41+
42+
def generate_stats(self, request, response):
43+
cleansed = request.POST.copy()
44+
for k in cleansed:
45+
cleansed[k] = CLEANSED_SUBSTITUTE
46+
self.record_stats(
47+
{
48+
"request_url": request.get_full_path(),
49+
"request_method": request.method,
50+
"post": json.dumps(cleansed, sort_keys=True, indent=4),
51+
"time": timezone.now(),
52+
}
53+
)
54+
55+
@property
56+
def content(self):
57+
"""Content of the panel when it's displayed in full screen.
58+
59+
Fetch every store for the toolbar and include it in the template.
60+
"""
61+
stores = OrderedDict()
62+
for id, toolbar in reversed(self.toolbar._store.items()):
63+
stores[id] = {
64+
"toolbar": toolbar,
65+
"form": HistoryStoreForm(initial={"store_id": id}),
66+
}
67+
68+
return render_to_string(
69+
self.template,
70+
{
71+
"current_store_id": self.toolbar.store_id,
72+
"stores": stores,
73+
"truncate_length": dt_settings.get_config()[
74+
"HISTORY_POST_TRUNCATE_LENGTH"
75+
],
76+
},
77+
)

debug_toolbar/panels/history/views.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from django.http import HttpResponseBadRequest, JsonResponse
2+
from django.template.loader import render_to_string
3+
from django.views.decorators.csrf import csrf_exempt
4+
5+
from debug_toolbar.decorators import require_show_toolbar
6+
from debug_toolbar.panels.history.forms import HistoryStoreForm
7+
from debug_toolbar.toolbar import DebugToolbar
8+
9+
10+
@csrf_exempt
11+
@require_show_toolbar
12+
def history_sidebar(request):
13+
"""Returns the selected debug toolbar history snapshot."""
14+
form = HistoryStoreForm(request.POST or None)
15+
16+
if form.is_valid():
17+
store_id = form.cleaned_data["store_id"]
18+
toolbar = DebugToolbar.fetch(store_id)
19+
context = {}
20+
for panel in toolbar.panels:
21+
if not panel.is_historical:
22+
continue
23+
panel_context = {"panel": panel}
24+
context[panel.panel_id] = {
25+
"button": render_to_string(
26+
"debug_toolbar/includes/panel_button.html", panel_context
27+
),
28+
"content": render_to_string(
29+
"debug_toolbar/includes/panel_content.html", panel_context
30+
),
31+
}
32+
return JsonResponse(context)
33+
return HttpResponseBadRequest("Form errors")

debug_toolbar/panels/logging.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ def __init__(self, *args, **kwargs):
6464

6565
@property
6666
def nav_subtitle(self):
67-
records = self._records[threading.currentThread()]
68-
record_count = len(records)
67+
stats = self.get_stats()
68+
record_count = len(stats["records"]) if stats else None
6969
return __("%(count)s message", "%(count)s messages", record_count) % {
7070
"count": record_count
7171
}

debug_toolbar/panels/staticfiles.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ def disable_instrumentation(self):
9999

100100
@property
101101
def num_used(self):
102-
return len(self._paths[threading.currentThread()])
102+
stats = self.get_stats()
103+
return stats and stats["num_used"]
103104

104105
nav_title = _("Static files")
105106

@@ -121,7 +122,7 @@ def generate_stats(self, request, response):
121122
self.record_stats(
122123
{
123124
"num_found": self.num_found,
124-
"num_used": self.num_used,
125+
"num_used": len(used_paths),
125126
"staticfiles": used_paths,
126127
"staticfiles_apps": self.get_staticfiles_apps(),
127128
"staticfiles_dirs": self.get_staticfiles_dirs(),

debug_toolbar/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"django.utils.deprecation",
3838
"django.utils.functional",
3939
),
40+
"HISTORY_POST_TRUNCATE_LENGTH": 0,
4041
"PROFILER_MAX_DEPTH": 10,
4142
"SHOW_TEMPLATE_CONTEXT": True,
4243
"SKIP_TEMPLATE_PREFIXES": ("django/forms/widgets/", "admin/widgets/"),
@@ -53,6 +54,7 @@ def get_config():
5354

5455

5556
PANELS_DEFAULTS = [
57+
"debug_toolbar.panels.history.HistoryPanel",
5658
"debug_toolbar.panels.versions.VersionsPanel",
5759
"debug_toolbar.panels.timer.TimerPanel",
5860
"debug_toolbar.panels.settings.SettingsPanel",

debug_toolbar/static/debug_toolbar/js/toolbar.js

+41-1
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,23 @@ const ajax = function(url, init) {
5555
});
5656
};
5757

58+
const ajaxJson = function(url, init) {
59+
init = Object.assign({credentials: 'same-origin'}, init);
60+
return fetch(url, init).then(function(response) {
61+
if (response.ok) {
62+
return response.json();
63+
} else {
64+
return Promise.reject();
65+
}
66+
});
67+
};
68+
5869
const djdt = {
5970
handleDragged: false,
6071
init: function() {
6172
const djDebug = document.querySelector('#djDebug');
6273
$$.show(djDebug);
63-
$$.on(djDebug.querySelector('#djDebugPanelList'), 'click', 'li a', function(event) {
74+
$$.on(djDebug.querySelector('#djDebugPanelList li a'), 'click', 'li a', function(event) {
6475
event.preventDefault();
6576
if (!this.className) {
6677
return;
@@ -128,6 +139,35 @@ const djdt = {
128139
});
129140
});
130141

142+
// Used by the history panel
143+
$$.on(djDebug, 'click', '.switchHistory', function(event) {
144+
event.preventDefault();
145+
const ajax_data = {};
146+
const newStoreId = this.dataset.storeId;
147+
const form = this.closest('form');
148+
const tbody = this.closest('tbody');
149+
150+
ajax_data.url = this.getAttribute('formaction');
151+
152+
if (form) {
153+
ajax_data.body = new FormData(form);
154+
ajax_data.method = form.getAttribute('method') || 'POST';
155+
}
156+
157+
tbody.querySelector('.djdt-highlighted').classList.remove('djdt-highlighted');
158+
this.closest('tr').classList.add('djdt-highlighted');
159+
160+
ajaxJson(ajax_data.url, ajax_data).then(function(data) {
161+
djDebug.setAttribute('data-store-id', newStoreId);
162+
Object.keys(data).map(function (panelId) {
163+
if (djDebug.querySelector('#'+panelId)) {
164+
djDebug.querySelector('#'+panelId).outerHTML = data[panelId].content;
165+
djDebug.querySelector('.djdt-'+panelId).outerHTML = data[panelId].button;
166+
}
167+
});
168+
});
169+
});
170+
131171
// Used by the cache, profiling and SQL panels
132172
$$.on(djDebug, 'click', 'a.djToggleSwitch', function(event) {
133173
event.preventDefault();

debug_toolbar/templates/debug_toolbar/base.html

+3-35
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,7 @@
1414
<li id="djDebugButton">DEBUG</li>
1515
{% endif %}
1616
{% for panel in toolbar.panels %}
17-
<li class="djDebugPanelButton">
18-
<input type="checkbox" data-cookie="djdt{{ panel.panel_id }}" {% if panel.enabled %}checked title="{% trans "Disable for next and successive requests" %}"{% else %}title="{% trans "Enable for next and successive requests" %}"{% endif %}>
19-
{% if panel.has_content and panel.enabled %}
20-
<a href="#" title="{{ panel.title }}" class="{{ panel.panel_id }}">
21-
{% else %}
22-
<div class="djdt-contentless{% if not panel.enabled %} djdt-disabled{% endif %}">
23-
{% endif %}
24-
{{ panel.nav_title }}
25-
{% if panel.enabled %}
26-
{% with panel.nav_subtitle as subtitle %}
27-
{% if subtitle %}<br><small>{{ subtitle }}</small>{% endif %}
28-
{% endwith %}
29-
{% endif %}
30-
{% if panel.has_content and panel.enabled %}
31-
</a>
32-
{% else %}
33-
</div>
34-
{% endif %}
35-
</li>
17+
{% include "debug_toolbar/includes/panel_button.html" %}
3618
{% endfor %}
3719
</ul>
3820
</div>
@@ -41,23 +23,9 @@
4123
<span id="djShowToolBarD">D</span><span id="djShowToolBarJ">J</span>DT
4224
</div>
4325
</div>
26+
4427
{% for panel in toolbar.panels %}
45-
{% if panel.has_content and panel.enabled %}
46-
<div id="{{ panel.panel_id }}" class="djdt-panelContent">
47-
<div class="djDebugPanelTitle">
48-
<a href="" class="djDebugClose">×</a>
49-
<h3>{{ panel.title|safe }}</h3>
50-
</div>
51-
<div class="djDebugPanelContent">
52-
{% if toolbar.store_id %}
53-
<img src="{% static 'debug_toolbar/img/ajax-loader.gif' %}" alt="loading" class="djdt-loader">
54-
<div class="djdt-scroll"></div>
55-
{% else %}
56-
<div class="djdt-scroll">{{ panel.content }}</div>
57-
{% endif %}
58-
</div>
59-
</div>
60-
{% endif %}
28+
{% include "debug_toolbar/includes/panel_content.html" %}
6129
{% endfor %}
6230
<div id="djDebugWindow" class="djdt-panelContent"></div>
6331
</div>

0 commit comments

Comments
 (0)