Skip to content

Commit ab08d14

Browse files
committed
* 'history' of https://github.com/tim-schilling/django-debug-toolbar: Fix documentation error, remove request variable cleansing. Handle python 3.5's json.loads expecting a string. Show history request data in table and include refresh history button. Add HistoryPanel to capture ajax requests.
2 parents e3a0758 + 4aa4dfe commit ab08d14

27 files changed

+620
-69
lines changed

debug_toolbar/middleware.py

+14-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) or request.path.startswith("/__debug__/"):
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]
@@ -75,8 +77,12 @@ def __call__(self, request):
7577
getattr(response, "streaming", False),
7678
"gzip" in content_encoding,
7779
content_type not in _HTML_TYPES,
80+
request.is_ajax(),
7881
)
7982
):
83+
# If a AJAX or JSON request, render the toolbar for the history.
84+
if request.is_ajax() or content_type == "application/json":
85+
toolbar.render_toolbar()
8086
return response
8187

8288
# Insert the toolbar in the response.
@@ -85,15 +91,6 @@ def __call__(self, request):
8591
pattern = re.escape(insert_before)
8692
bits = re.split(pattern, content, flags=re.IGNORECASE)
8793
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-
9794
bits[-2] += toolbar.render_toolbar()
9895
response.content = insert_before.join(bits)
9996
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

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
@staticmethod
30+
def make_hash(data):
31+
m = hmac.new(key=force_bytes(settings.SECRET_KEY), digestmod=hashlib.sha1)
32+
m.update(force_bytes(data["store_id"]))
33+
return m.hexdigest()
34+
35+
def clean_hash(self):
36+
hash = self.cleaned_data["hash"]
37+
38+
if not constant_time_compare(hash, self.make_hash(self.data)):
39+
raise ValidationError("Tamper alert")
40+
41+
return hash

debug_toolbar/panels/history/panel.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import json
2+
import logging
3+
import sys
4+
from collections import OrderedDict
5+
6+
from django.conf import settings
7+
from django.conf.urls import url
8+
from django.template.loader import render_to_string
9+
from django.utils import timezone
10+
from django.utils.translation import gettext_lazy as _
11+
12+
from debug_toolbar.panels import Panel
13+
from debug_toolbar.panels.history import views
14+
from debug_toolbar.panels.history.forms import HistoryStoreForm
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class HistoryPanel(Panel):
20+
""" A panel to display History """
21+
22+
title = _("History")
23+
nav_title = _("History")
24+
template = "debug_toolbar/panels/history.html"
25+
26+
@property
27+
def is_historical(self):
28+
"""The HistoryPanel should not be included in the historical panels."""
29+
return False
30+
31+
@classmethod
32+
def get_urls(cls):
33+
return [
34+
url(r"^history_sidebar/$", views.history_sidebar, name="history_sidebar"),
35+
url(r"^history_refresh/$", views.history_refresh, name="history_refresh"),
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+
if request.method == "GET":
44+
data = request.GET.copy()
45+
else:
46+
data = request.POST.copy()
47+
# GraphQL tends to not be populated in POST. If the request seems
48+
# empty, check if it's a JSON request.
49+
if not data and request.META.get("CONTENT_TYPE") == "application/json":
50+
# Python <= 3.5's json.loads expects a string.
51+
data = json.loads(
52+
request.body
53+
if sys.version_info[:2] > (3, 5)
54+
else request.body.decode(request.encoding or settings.DEFAULT_CHARSET)
55+
)
56+
self.record_stats(
57+
{
58+
"request_url": request.get_full_path(),
59+
"request_method": request.method,
60+
"data": data,
61+
"time": timezone.now(),
62+
}
63+
)
64+
65+
@property
66+
def content(self):
67+
"""Content of the panel when it's displayed in full screen.
68+
69+
Fetch every store for the toolbar and include it in the template.
70+
"""
71+
stores = OrderedDict()
72+
for id, toolbar in reversed(self.toolbar._store.items()):
73+
stores[id] = {
74+
"toolbar": toolbar,
75+
"form": HistoryStoreForm(initial={"store_id": id}),
76+
}
77+
78+
return render_to_string(
79+
self.template,
80+
{
81+
"current_store_id": self.toolbar.store_id,
82+
"stores": stores,
83+
"refresh_form": HistoryStoreForm(
84+
initial={"store_id": self.toolbar.store_id}
85+
),
86+
},
87+
)

debug_toolbar/panels/history/views.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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")
34+
35+
36+
@csrf_exempt
37+
@require_show_toolbar
38+
def history_refresh(request):
39+
"""Returns the refreshed list of table rows for the History Panel."""
40+
form = HistoryStoreForm(request.POST or None)
41+
42+
if form.is_valid():
43+
requests = []
44+
for id, toolbar in reversed(DebugToolbar._store.items()):
45+
requests.append(
46+
{
47+
"id": id,
48+
"content": render_to_string(
49+
"debug_toolbar/panels/history_tr.html",
50+
{
51+
"id": id,
52+
"store_context": {
53+
"toolbar": toolbar,
54+
"form": HistoryStoreForm(initial={"store_id": id}),
55+
},
56+
},
57+
),
58+
}
59+
)
60+
61+
return JsonResponse({"requests": requests})
62+
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-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
},
1818
"INSERT_BEFORE": "</body>",
1919
"RENDER_PANELS": None,
20-
"RESULTS_CACHE_SIZE": 10,
20+
"RESULTS_CACHE_SIZE": 25,
2121
"ROOT_TAG_EXTRA_ATTRS": "",
2222
"SHOW_COLLAPSED": False,
2323
"SHOW_TOOLBAR_CALLBACK": "debug_toolbar.middleware.show_toolbar",
@@ -53,6 +53,7 @@ def get_config():
5353

5454

5555
PANELS_DEFAULTS = [
56+
"debug_toolbar.panels.history.HistoryPanel",
5657
"debug_toolbar.panels.versions.VersionsPanel",
5758
"debug_toolbar.panels.timer.TimerPanel",
5859
"debug_toolbar.panels.settings.SettingsPanel",

debug_toolbar/static/debug_toolbar/js/toolbar.js

+54
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,60 @@ const djdt = {
128128
});
129129
});
130130

131+
// Used by the history panel
132+
$$.on(djDebug, 'click', '.switchHistory', function(event) {
133+
event.preventDefault();
134+
const ajax_data = {};
135+
const newStoreId = this.dataset.storeId;
136+
const form = this.closest('form');
137+
const tbody = this.closest('tbody');
138+
139+
ajax_data.url = this.getAttribute('formaction');
140+
141+
if (form) {
142+
ajax_data.body = new FormData(form);
143+
ajax_data.method = form.getAttribute('method') || 'POST';
144+
}
145+
146+
tbody.querySelector('.djdt-highlighted').classList.remove('djdt-highlighted');
147+
this.closest('tr').classList.add('djdt-highlighted');
148+
149+
ajax(ajax_data.url, ajax_data).then(function(data) {
150+
djDebug.setAttribute('data-store-id', newStoreId);
151+
Object.keys(data).map(function (panelId) {
152+
if (djDebug.querySelector('#'+panelId)) {
153+
djDebug.querySelector('#'+panelId).outerHTML = data[panelId].content;
154+
djDebug.querySelector('.djdt-'+panelId).outerHTML = data[panelId].button;
155+
}
156+
});
157+
});
158+
});
159+
160+
// Used by the history panel
161+
$$.on(djDebug, 'click', '.refreshHistory', function(event) {
162+
event.preventDefault();
163+
const ajax_data = {};
164+
const form = this.closest('form');
165+
const container = djDebug.querySelector('#djdtHistoryRequests');
166+
167+
ajax_data.url = this.getAttribute('formaction');
168+
169+
if (form) {
170+
ajax_data.body = new FormData(form);
171+
ajax_data.method = form.getAttribute('method') || 'POST';
172+
}
173+
174+
ajax(ajax_data.url, ajax_data).then(function(data) {
175+
if (data.requests.constructor === Array) {
176+
data.requests.map(function(request) {
177+
if (!container.querySelector('[data-store-id="'+request.id+'"]')) {
178+
container.innerHTML = request.content + container.innerHTML;
179+
}
180+
});
181+
}
182+
});
183+
});
184+
131185
// Used by the cache, profiling and SQL panels
132186
$$.on(djDebug, 'click', 'a.djToggleSwitch', function(event) {
133187
event.preventDefault();

0 commit comments

Comments
 (0)