Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialize the panels #1826

Closed
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
e7cf575
Ignore common venv folder.
tim-schilling Aug 20, 2023
c4201fa
Rename store_id variants to request_id
tim-schilling Aug 20, 2023
bbbbb34
Support serializable panels. This is a WIP and needs clean-up.
tim-schilling Aug 20, 2023
e2f695b
Support serializable sql panel
tim-schilling Aug 21, 2023
14a5e0c
Make Panel.panel_id a classmember.
tim-schilling Aug 21, 2023
a31115f
Force everything to a string if it can't be serialized.
tim-schilling Aug 29, 2023
71edcf5
Support serialization of FunctionCall
tim-schilling Aug 29, 2023
a344da1
Add note on lack of async support to docs (#1829)
salomvary Aug 30, 2023
199c2b3
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Aug 28, 2023
c03f08f
Update all panels to use data from get_stats on render
tim-schilling Sep 5, 2023
47bdabe
Extend example app to have an async version.
tim-schilling Sep 5, 2023
5d5d459
[pre-commit.ci] pre-commit autoupdate (#1830)
pre-commit-ci[bot] Sep 26, 2023
06c42d9
do not quote SQL params before passing them to mogrify (#1832)
tkoschnick Sep 26, 2023
3ceb965
panels(templates): avoid evaluating LazyObject (#1833)
nijel Sep 26, 2023
58293b4
Support Django 5.0
adamchainz Oct 13, 2023
036e8db
Add Python 3.12 to the test matrix. (#1816)
tim-schilling Oct 14, 2023
d4cfadc
[pre-commit.ci] pre-commit autoupdate (#1838)
pre-commit-ci[bot] Oct 19, 2023
e58e78b
Fix `utils.get_name_from_obj` proper view names (#1846)
leandrodesouzadev Oct 23, 2023
3bcae73
[pre-commit.ci] pre-commit autoupdate (#1849)
pre-commit-ci[bot] Oct 24, 2023
e1658db
pyproject.toml: Work on the readability of ruff settings (#1850)
cclauss Oct 27, 2023
94c5219
Switch to ruff format (#1852)
matthiask Oct 27, 2023
473186d
[pre-commit.ci] pre-commit autoupdate (#1855)
pre-commit-ci[bot] Nov 6, 2023
eea6437
[pre-commit.ci] pre-commit autoupdate (#1856)
pre-commit-ci[bot] Nov 20, 2023
f0a0426
Fix #1858 -- Drop support for Django 4.0 (#1859)
pauloxnet Nov 28, 2023
d6b8e22
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Dec 4, 2023
6ff1dd1
Enable the temporary workaround for https://github.com/prettier/prett…
matthiask Dec 4, 2023
be989d2
Fix #1860 -- Update GitHub action versions (#1861)
pauloxnet Dec 4, 2023
fb16de1
[pre-commit.ci] pre-commit autoupdate (#1864)
pre-commit-ci[bot] Dec 18, 2023
6e9ce48
Refactor is_project_func method for Windows compatibility (#1857)
DraKen0009 Dec 18, 2023
075d38b
[pre-commit.ci] pre-commit autoupdate (#1866)
pre-commit-ci[bot] Dec 25, 2023
7731cd2
Configure ESLint using a JS file instead of JSON (#1868)
matthiask Jan 3, 2024
77aa47a
pre-commit-config: Upgrade ruff (#1869)
cclauss Jan 9, 2024
22df01c
The djdt handle shouldn't be stuck at the top of the browser window i…
VeldaKiara Jan 25, 2024
0e7711e
1843 new ajax request reset's whole view if history panel is enabled …
elineda Jan 29, 2024
0b59e24
Version 4.3.0
tim-schilling Feb 1, 2024
b041e7c
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Feb 5, 2024
6591d02
Make ruff complain less
matthiask Feb 5, 2024
757b82e
[pre-commit.ci] pre-commit autoupdate (#1867)
pre-commit-ci[bot] Feb 6, 2024
c688ce4
Use url template tag for example URLs (#1879)
tim-schilling Feb 11, 2024
64697a4
Keep GitHub Actions up to date with GitHub's Dependabot (#1876)
cclauss Feb 13, 2024
14fbaa8
Bump the github-actions group with 4 updates (#1885)
dependabot[bot] Feb 14, 2024
38d2eea
#1870 fix pre commit errors (#1884)
elijah0kello Feb 14, 2024
0663276
[pre-commit.ci] pre-commit autoupdate (#1888)
pre-commit-ci[bot] Feb 22, 2024
7d77a34
Show toolbar for docker's internal IP address (#1887)
tim-schilling Feb 22, 2024
e80c05d
Raise the minimum Django version to 4.2 (#1880)
matthiask Feb 22, 2024
9f66bd3
Improve handling when djdt views dont respond with JSON (#1877)
elineda Feb 22, 2024
b9e4af7
Fix DeprecationWarnings about form default templates (#1878)
pfouque Feb 22, 2024
eb0b6ea
Update pre-commit hooks
matthiask Mar 5, 2024
0baef8c
Add architecture documentation for the project.
tim-schilling Mar 3, 2024
cc48a14
Remove obsolete staticfiles check
living180 Mar 11, 2024
cfd4801
Make tox pass selenium environment variables
living180 Mar 11, 2024
68039c6
Simplify default OBSERVE_REQUEST_CALLBACK behavior
living180 Mar 11, 2024
cfbad48
Deprecate OBSERVE_REQUEST_CALLBACK
living180 Mar 11, 2024
b3cb611
[pre-commit.ci] pre-commit autoupdate (#1896)
pre-commit-ci[bot] Mar 12, 2024
3097692
Allow more control over tox Selenium tests (#1897)
living180 Mar 18, 2024
c2daf3a
Merge pull request #1895 from living180/observe_request_callback
matthiask Mar 18, 2024
d3f4dbd
[pre-commit.ci] pre-commit autoupdate (#1898)
pre-commit-ci[bot] Mar 18, 2024
e8848dc
Docs > Add a note on the profiling panel doc (#1899)
elineda Mar 21, 2024
4c4f767
[pre-commit.ci] pre-commit autoupdate (#1900)
pre-commit-ci[bot] Mar 30, 2024
6d0535f
[pre-commit.ci] pre-commit autoupdate (#1902)
pre-commit-ci[bot] Apr 9, 2024
a36bbba
[pre-commit.ci] pre-commit autoupdate (#1907)
pre-commit-ci[bot] Apr 15, 2024
2f4c471
'djdt' is not a registered namespace #1405 (#1889)
VeldaKiara Apr 30, 2024
2134600
Remove unnecessary GitHub Graph info (#1910)
jeffwidman Apr 30, 2024
c1463a5
New coverage.yml for code coverage (#1912)
salty-ivy May 7, 2024
7271cac
Dark mode support (#1913)
TheRealVizard May 14, 2024
8391469
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] May 20, 2024
2990290
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 20, 2024
de2feca
Fix theme selenium integration test.
tim-schilling May 26, 2024
15dc305
Version 4.4.0
tim-schilling May 26, 2024
202c831
Limit metadata version for Jazzband's release process
tim-schilling May 26, 2024
25c860e
Version 4.4.1
tim-schilling May 26, 2024
97b49d1
Merge remote-tracking branch 'origin/pre-commit-ci-update-config'
matthiask May 27, 2024
e7541ab
Ignore UP031 for now
matthiask May 27, 2024
4808add
Avoid setting color-scheme on :root, we're only a guest on pages (#1923)
matthiask May 27, 2024
782bdd9
INTERNAL_IPS may not be a list
matthiask May 27, 2024
f7e83b1
Add a section to the installation docs about running tests (#1921)
matthiask May 27, 2024
d481182
Version 4.4.2
matthiask May 27, 2024
a0f0df9
Fix overriding font-family for both light and dark themes (#1930)
federicobond Jun 1, 2024
cf0cc0d
Restore compatibility with iptools.IpRangeList (#1929)
quinox Jun 1, 2024
9c4eb67
Amend the changelog
matthiask Jun 1, 2024
b5cf784
[pre-commit.ci] pre-commit autoupdate (#1931)
pre-commit-ci[bot] Jun 10, 2024
0a622dc
[pre-commit.ci] pre-commit autoupdate (#1934)
pre-commit-ci[bot] Jun 17, 2024
2b07905
[pre-commit.ci] pre-commit autoupdate (#1939)
pre-commit-ci[bot] Jun 24, 2024
c54f898
[pre-commit.ci] pre-commit autoupdate (#1940)
pre-commit-ci[bot] Jul 1, 2024
2d9c6a7
Limit the cases for E001 to likely scenarios (#1925)
tim-schilling Jul 3, 2024
325ea19
Introduce debug_toolbar_urls to simplify installation (#1926)
tim-schilling Jul 3, 2024
d0f09b3
Fixed #1682 -- alert user when using file field without proper encodi…
bkdekoning Jul 4, 2024
eff9128
Remove rem units from svg (#1942)
michjnich Jul 4, 2024
c79f249
Version 4.4.3
tim-schilling Jul 4, 2024
afeee86
Check for for StreamingHttpResponse when generating stats in Alert (#…
danjac Jul 5, 2024
e59c8ca
Clean-up change log.
tim-schilling Jul 5, 2024
c8f6763
Instrument the Django Jinja2 template backend.
matthiask Feb 12, 2024
4fd886b
Improve the jinja tests to better indicate the situation.
tim-schilling Jul 5, 2024
6614910
Fix jinja2 integration test.
tim-schilling Jul 5, 2024
9834e7e
Ignore check for jinja2's base.html template in integration test
tim-schilling Jul 5, 2024
57ada8e
Version 4.4.4
tim-schilling Jul 5, 2024
a591d86
Fix #1951: Do not crash if the 'alerts' key doesn't exist (#1953)
matthiask Jul 5, 2024
944120c
Only import the jinja2 instrumentation when jinja2 itself is importab…
matthiask Jul 5, 2024
7acad6b
django-debug-toolbar 4.4.5
matthiask Jul 5, 2024
dfad5db
Close #1509: Revert the infinite recursion fix, Django has changed th…
matthiask Jul 5, 2024
699c1d9
Fixed order and grammatical number of panels in documentation (#1956)
bkdekoning Jul 6, 2024
9bcd6ca
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Jul 8, 2024
982a127
Alerts panel: Only process HTML responses
matthiask Jul 9, 2024
dd53424
Merge branch 'main' into serialize-panels
tim-schilling Jul 10, 2024
16e02f5
Rework the alerts panel to be compatible with serialization.
tim-schilling Jul 10, 2024
3e4c484
Make template panel serializable.
tim-schilling Jul 10, 2024
f4ff5f4
Avoid caching the config settings.
tim-schilling Jul 11, 2024
d3730a6
Fix tests for serializable changes with selenium.
tim-schilling Jul 11, 2024
c660269
Comment out the async button because it breaks the wsgi app.
tim-schilling Jul 11, 2024
8402c4d
Hack: Sleep before checking to see if the history panel auto updated.
tim-schilling Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ htmlcov
.tox
geckodriver.log
coverage.xml
venv
26 changes: 23 additions & 3 deletions debug_toolbar/panels/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.template.loader import render_to_string
from django.utils.functional import classproperty

from debug_toolbar import settings as dt_settings
from debug_toolbar.utils import get_name_from_obj
Expand All @@ -12,15 +13,22 @@ class Panel:
def __init__(self, toolbar, get_response):
self.toolbar = toolbar
self.get_response = get_response
self.from_store = False

# Private panel properties

@property
def panel_id(self):
return self.__class__.__name__
@classproperty
def panel_id(cls):
return cls.__name__

@property
def enabled(self) -> bool:
if self.from_store:
# If the toolbar was loaded from the store the existence of
# recorded data indicates whether it was enabled or not.
# We can't use the remainder of the logic since we don't have
# a request to work off of.
return bool(self.get_stats())
# The user's cookies should override the default value
cookie_value = self.toolbar.request.COOKIES.get("djdt" + self.panel_id)
if cookie_value is not None:
Expand Down Expand Up @@ -168,6 +176,9 @@ def record_stats(self, stats):
Each call to ``record_stats`` updates the statistics dictionary.
"""
self.toolbar.stats.setdefault(self.panel_id, {}).update(stats)
self.toolbar.store.save_panel(
self.toolbar.request_id, self.panel_id, self.toolbar.stats[self.panel_id]
)

def get_stats(self):
"""
Expand Down Expand Up @@ -251,6 +262,15 @@ def generate_server_timing(self, request, response):
Does not return a value.
"""

def load_stats_from_store(self, data):
"""
Instantiate the panel from serialized data.

Return the panel instance.
"""
self.toolbar.stats.setdefault(self.panel_id, {}).update(data)
self.from_store = True

@classmethod
def run_checks(cls):
"""
Expand Down
2 changes: 1 addition & 1 deletion debug_toolbar/panels/history/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from debug_toolbar.panels.history.panel import HistoryPanel

__all__ = ["HistoryPanel"]
__all__ = [HistoryPanel.panel_id]
4 changes: 2 additions & 2 deletions debug_toolbar/panels/history/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ class HistoryStoreForm(forms.Form):
"""
Validate params

store_id: The key for the store instance to be fetched.
request_id: The key for the store instance to be fetched.
"""

store_id = forms.CharField(widget=forms.HiddenInput())
request_id = forms.CharField(widget=forms.HiddenInput())
exclude_history = forms.BooleanField(widget=forms.HiddenInput(), required=False)
21 changes: 10 additions & 11 deletions debug_toolbar/panels/history/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ class HistoryPanel(Panel):
def get_headers(self, request):
headers = super().get_headers(request)
observe_request = self.toolbar.get_observe_request()
store_id = self.toolbar.store_id
if store_id and observe_request(request):
headers["djdt-store-id"] = store_id
request_id = self.toolbar.request_id
if request_id and observe_request(request):
headers["djdt-request-id"] = request_id
return headers

@property
Expand Down Expand Up @@ -86,23 +86,22 @@ def content(self):

Fetch every store for the toolbar and include it in the template.
"""
stores = {}
for id, toolbar in reversed(self.toolbar._store.items()):
stores[id] = {
"toolbar": toolbar,
toolbar_history = {}
for request_id in reversed(self.toolbar.store.request_ids()):
toolbar_history[request_id] = {
"form": HistoryStoreForm(
initial={"store_id": id, "exclude_history": True}
initial={"request_id": request_id, "exclude_history": True}
),
}

return render_to_string(
self.template,
{
"current_store_id": self.toolbar.store_id,
"stores": stores,
"current_request_id": self.toolbar.request_id,
"toolbar_history": toolbar_history,
"refresh_form": HistoryStoreForm(
initial={
"store_id": self.toolbar.store_id,
"request_id": self.toolbar.request_id,
"exclude_history": True,
}
),
Expand Down
16 changes: 9 additions & 7 deletions debug_toolbar/panels/history/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar
from debug_toolbar.panels.history.forms import HistoryStoreForm
from debug_toolbar.store import get_store
from debug_toolbar.toolbar import DebugToolbar


Expand All @@ -13,12 +14,12 @@ def history_sidebar(request):
form = HistoryStoreForm(request.GET)

if form.is_valid():
store_id = form.cleaned_data["store_id"]
toolbar = DebugToolbar.fetch(store_id)
request_id = form.cleaned_data["request_id"]
toolbar = DebugToolbar.fetch(request_id)
exclude_history = form.cleaned_data["exclude_history"]
context = {}
if toolbar is None:
# When the store_id has been popped already due to
# When the request_id has been popped already due to
# RESULTS_CACHE_SIZE
return JsonResponse(context)
for panel in toolbar.panels:
Expand Down Expand Up @@ -46,19 +47,20 @@ def history_refresh(request):
if form.is_valid():
requests = []
# Convert to list to handle mutations happening in parallel
for id, toolbar in list(DebugToolbar._store.items()):
for request_id in get_store().request_ids():
toolbar = DebugToolbar.fetch(request_id)
requests.append(
{
"id": id,
"id": request_id,
"content": render_to_string(
"debug_toolbar/panels/history_tr.html",
{
"id": id,
"id": request_id,
"store_context": {
"toolbar": toolbar,
"form": HistoryStoreForm(
initial={
"store_id": id,
"request_id": request_id,
"exclude_history": True,
}
),
Expand Down
10 changes: 9 additions & 1 deletion debug_toolbar/panels/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.conf import settings
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from django.views.debug import get_default_exception_reporter_filter

Expand All @@ -20,4 +21,11 @@ def title(self):
return _("Settings from %s") % settings.SETTINGS_MODULE

def generate_stats(self, request, response):
self.record_stats({"settings": dict(sorted(get_safe_settings().items()))})
self.record_stats(
{
"settings": {
key: force_str(value)
for key, value in sorted(get_safe_settings().items())
}
}
)
2 changes: 1 addition & 1 deletion debug_toolbar/panels/sql/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from debug_toolbar.panels.sql.panel import SQLPanel

__all__ = ["SQLPanel"]
__all__ = [SQLPanel.panel_id]
98 changes: 87 additions & 11 deletions debug_toolbar/panels/sql/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,22 @@
from django.core.exceptions import ValidationError
from django.db import connections
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _

from debug_toolbar.panels.sql.utils import reformat_sql
from debug_toolbar.toolbar import DebugToolbar


class SQLSelectForm(forms.Form):
"""
Validate params

sql: The sql statement with interpolated params
raw_sql: The sql statement with placeholders
params: JSON encoded parameter values
duration: time for SQL to execute passed in from toolbar just for redisplay
request_id: The identifier for the request
query_id: The identifier for the query
"""

sql = forms.CharField()
raw_sql = forms.CharField()
params = forms.CharField()
alias = forms.CharField(required=False, initial="default")
duration = forms.FloatField()
request_id = forms.CharField()
djdt_query_id = forms.CharField()

def clean_raw_sql(self):
value = self.cleaned_data["raw_sql"]
Expand All @@ -48,12 +45,91 @@ def clean_alias(self):

return value

def clean(self):
from debug_toolbar.panels.sql import SQLPanel

cleaned_data = super().clean()
toolbar = DebugToolbar.fetch(
self.cleaned_data["request_id"], panel_id=SQLPanel.panel_id
)
if toolbar is None:
raise ValidationError(_("Data for this panel isn't available anymore."))

panel = toolbar.get_panel_by_id(SQLPanel.panel_id)
# Find the query for this form submission
query = None
for q in panel.get_stats()["queries"]:
if q["djdt_query_id"] != self.cleaned_data["djdt_query_id"]:
continue
else:
query = q
break
if not query:
raise ValidationError(_("Invalid query id."))
cleaned_data["query"] = query
return cleaned_data

def select(self):
query = self.cleaned_data["query"]
sql = query["raw_sql"]
params = json.loads(query["params"])
with self.cursor as cursor:
cursor.execute(sql, params)
headers = [d[0] for d in cursor.description]
result = cursor.fetchall()
return result, headers

def explain(self):
query = self.cleaned_data["query"]
sql = query["raw_sql"]
params = json.loads(query["params"])
vendor = query["vendor"]
with self.cursor as cursor:
if vendor == "sqlite":
# SQLite's EXPLAIN dumps the low-level opcodes generated for a query;
# EXPLAIN QUERY PLAN dumps a more human-readable summary
# See https://www.sqlite.org/lang_explain.html for details
cursor.execute(f"EXPLAIN QUERY PLAN {sql}", params)
elif vendor == "postgresql":
cursor.execute(f"EXPLAIN ANALYZE {sql}", params)
else:
cursor.execute(f"EXPLAIN {sql}", params)
headers = [d[0] for d in cursor.description]
result = cursor.fetchall()
return result, headers

def profile(self):
query = self.cleaned_data["query"]
sql = query["raw_sql"]
params = json.loads(query["params"])
with self.cursor as cursor:
cursor.execute("SET PROFILING=1") # Enable profiling
cursor.execute(sql, params) # Execute SELECT
cursor.execute("SET PROFILING=0") # Disable profiling
# The Query ID should always be 1 here but I'll subselect to get
# the last one just in case...
cursor.execute(
"""
SELECT *
FROM information_schema.profiling
WHERE query_id = (
SELECT query_id
FROM information_schema.profiling
ORDER BY query_id DESC
LIMIT 1
)
"""
)
headers = [d[0] for d in cursor.description]
result = cursor.fetchall()
return result, headers

def reformat_sql(self):
return reformat_sql(self.cleaned_data["sql"], with_toggle=False)
return reformat_sql(self.cleaned_data["query"]["sql"], with_toggle=False)

@property
def connection(self):
return connections[self.cleaned_data["alias"]]
return connections[self.cleaned_data["query"]["alias"]]

@cached_property
def cursor(self):
Expand Down
Loading