diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b7cd30c3..667846cdc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -216,4 +216,4 @@ jobs: python -m pip install --upgrade tox - name: Test with tox - run: tox -e docs,style,readme + run: tox -e docs,style,packaging diff --git a/.gitignore b/.gitignore index 564e7b8cc..df5a2d10c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.pyc *.DS_Store *~ +.idea build .coverage dist diff --git a/README.rst b/README.rst index 3a3304780..8bea757ad 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ Here's a screenshot of the toolbar in action: In addition to the built-in panels, a number of third-party panels are contributed by the community. -The current stable version of the Debug Toolbar is 3.2.1. It works on +The current stable version of the Debug Toolbar is 3.2.2. It works on Django ≥ 2.2. Documentation, including installation and configuration instructions, is diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py index 84eea7d7a..93c78c7f0 100644 --- a/debug_toolbar/__init__.py +++ b/debug_toolbar/__init__.py @@ -1,12 +1,15 @@ +import django + __all__ = ["VERSION"] # Do not use pkg_resources to find the version but set it here directly! # see issue #1446 -VERSION = "3.2.1" +VERSION = "3.2.2" # Code that discovers files or modules in INSTALLED_APPS imports this module. urls = "debug_toolbar.toolbar", "djdt" -default_app_config = "debug_toolbar.apps.DebugToolbarConfig" +if django.VERSION < (3, 2): + default_app_config = "debug_toolbar.apps.DebugToolbarConfig" diff --git a/debug_toolbar/locale/sk/LC_MESSAGES/django.mo b/debug_toolbar/locale/sk/LC_MESSAGES/django.mo index a26e3cc7b..e6af7c505 100644 Binary files a/debug_toolbar/locale/sk/LC_MESSAGES/django.mo and b/debug_toolbar/locale/sk/LC_MESSAGES/django.mo differ diff --git a/debug_toolbar/locale/sk/LC_MESSAGES/django.po b/debug_toolbar/locale/sk/LC_MESSAGES/django.po index 2c8a9c7e3..ef6657953 100644 --- a/debug_toolbar/locale/sk/LC_MESSAGES/django.po +++ b/debug_toolbar/locale/sk/LC_MESSAGES/django.po @@ -1,7 +1,7 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# -# +# +# # Translators: # Juraj Bubniak , 2012 # Juraj Bubniak , 2013 @@ -11,23 +11,22 @@ msgstr "" "Project-Id-Version: Django Debug Toolbar\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-04-25 21:52+0200\n" -"PO-Revision-Date: 2014-04-25 19:53+0000\n" +"PO-Revision-Date: 2021-06-24 13:37+0200\n" "Last-Translator: Aymeric Augustin \n" "Language-Team: Slovak (http://www.transifex.com/projects/p/django-debug-toolbar/language/sk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: sk\n" -"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n" +"X-Generator: Poedit 2.4.2\n" #: apps.py:11 msgid "Debug Toolbar" -msgstr "" +msgstr "Debug Toolbar" #: views.py:14 -msgid "" -"Data for this panel isn't available anymore. Please reload the page and " -"retry." +msgid "Data for this panel isn't available anymore. Please reload the page and retry." msgstr "Dáta pre tento panel už nie sú k dispozícii. Načítajte si prosím stránku a skúste to znova." #: panels/cache.py:191 @@ -39,8 +38,9 @@ msgstr "Cache" msgid "%(cache_calls)d call in %(time).2fms" msgid_plural "%(cache_calls)d calls in %(time).2fms" msgstr[0] "%(cache_calls)d volanie za %(time).2fms" -msgstr[1] "%(cache_calls)d volaní za %(time).2fms" +msgstr[1] "%(cache_calls)d volania za %(time).2fms" msgstr[2] "%(cache_calls)d volaní za %(time).2fms" +msgstr[3] "%(cache_calls)d volaní za %(time).2fms" #: panels/cache.py:204 #, python-format @@ -48,7 +48,8 @@ msgid "Cache calls from %(count)d backend" msgid_plural "Cache calls from %(count)d backends" msgstr[0] "Cache volania z %(count)d backendu" msgstr[1] "Cache volania z %(count)d backendov" -msgstr[2] "Cache volania z %(count)d backendov" +msgstr[2] "Cache volania z %(count)d backendu" +msgstr[3] "Cache volania z %(count)d backendov" #: panels/headers.py:35 msgid "Headers" @@ -63,8 +64,9 @@ msgstr "Zápis" msgid "%(count)s message" msgid_plural "%(count)s messages" msgstr[0] "%(count)s správa" -msgstr[1] "%(count)s správ" -msgstr[2] "%(count)s správ" +msgstr[1] "%(count)s správy" +msgstr[2] "%(count)s správy" +msgstr[3] "%(count)s správ" #: panels/logging.py:73 msgid "Log messages" @@ -104,16 +106,18 @@ msgstr "Nastavenia z %s" msgid "%(num_receivers)d receiver of 1 signal" msgid_plural "%(num_receivers)d receivers of 1 signal" msgstr[0] "%(num_receivers)d príjemca 1 signálu" -msgstr[1] "%(num_receivers)d príjemcov 1 signálu" +msgstr[1] "%(num_receivers)d príjemcovia 1 signálu" msgstr[2] "%(num_receivers)d príjemcov 1 signálu" +msgstr[3] "%(num_receivers)d príjemcov 1 signálu" #: panels/signals.py:48 #, python-format msgid "%(num_receivers)d receiver of %(num_signals)d signals" msgid_plural "%(num_receivers)d receivers of %(num_signals)d signals" -msgstr[0] "%(num_receivers)d príjemca %(num_signals)d signálov" +msgstr[0] "%(num_receivers)d príjemca %(num_signals)d signálu" msgstr[1] "%(num_receivers)d príjemcov %(num_signals)d signálov" -msgstr[2] "%(num_receivers)d príjemcov %(num_signals)d signálov" +msgstr[2] "%(num_receivers)d príjemcu %(num_signals)d signálu" +msgstr[3] "%(num_receivers)d príjemcov %(num_signals)d signálov" #: panels/signals.py:53 msgid "Signals" @@ -133,8 +137,9 @@ msgstr "Statické súbory" msgid "%(num_used)s file used" msgid_plural "%(num_used)s files used" msgstr[0] "%(num_used)s použitý súbor" -msgstr[1] "%(num_used)s použitých súborov" +msgstr[1] "%(num_used)s použité súbory" msgstr[2] "%(num_used)s použitých súborov" +msgstr[3] "%(num_used)s použitých súborov" #: panels/timer.py:23 #, python-format @@ -146,10 +151,8 @@ msgstr "CPU: %(cum)0.2fms (%(total)0.2fms)" msgid "Total: %0.2fms" msgstr "Celkovo: %0.2fms" -#: panels/timer.py:34 templates/debug_toolbar/panels/logging.html:7 -#: templates/debug_toolbar/panels/sql_explain.html:11 -#: templates/debug_toolbar/panels/sql_profile.html:12 -#: templates/debug_toolbar/panels/sql_select.html:11 +#: panels/timer.py:34 templates/debug_toolbar/panels/logging.html:7 templates/debug_toolbar/panels/sql_explain.html:11 +#: templates/debug_toolbar/panels/sql_profile.html:12 templates/debug_toolbar/panels/sql_select.html:11 msgid "Time" msgstr "Čas" @@ -228,11 +231,11 @@ msgstr "Nečinný" #: panels/sql/panel.py:38 msgid "Active" -msgstr "Akcia" +msgstr "Aktívne" #: panels/sql/panel.py:39 msgid "In transaction" -msgstr "Stav transakcie:" +msgstr "V transakcii" #: panels/sql/panel.py:40 msgid "In error" @@ -285,9 +288,8 @@ msgstr "Poloha:" #: templates/debug_toolbar/redirect.html:10 msgid "" -"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." +"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." msgstr "Django Debug Toolbar zachytil presmerovanie na vyššie uvedenú URL pre účely ladenia. Pre normálne presmerovanie môžete kliknúť na vyššie uvedený odkaz." #: templates/debug_toolbar/panels/cache.html:2 @@ -318,8 +320,7 @@ msgstr "Príkazy" msgid "Calls" msgstr "Volania" -#: templates/debug_toolbar/panels/cache.html:43 -#: templates/debug_toolbar/panels/sql.html:20 +#: templates/debug_toolbar/panels/cache.html:43 templates/debug_toolbar/panels/sql.html:20 msgid "Time (ms)" msgstr "Čas (ms)" @@ -327,13 +328,11 @@ msgstr "Čas (ms)" msgid "Type" msgstr "Typ" -#: templates/debug_toolbar/panels/cache.html:45 -#: templates/debug_toolbar/panels/request.html:8 +#: templates/debug_toolbar/panels/cache.html:45 templates/debug_toolbar/panels/request.html:8 msgid "Arguments" msgstr "Argumenty" -#: templates/debug_toolbar/panels/cache.html:46 -#: templates/debug_toolbar/panels/request.html:9 +#: templates/debug_toolbar/panels/cache.html:46 templates/debug_toolbar/panels/request.html:9 msgid "Keyword arguments" msgstr "Kľúčové argumenty" @@ -345,21 +344,13 @@ msgstr "Backend" msgid "Request headers" msgstr "Hlavičky požiadavky" -#: templates/debug_toolbar/panels/headers.html:8 -#: templates/debug_toolbar/panels/headers.html:27 -#: templates/debug_toolbar/panels/headers.html:48 +#: templates/debug_toolbar/panels/headers.html:8 templates/debug_toolbar/panels/headers.html:27 templates/debug_toolbar/panels/headers.html:48 msgid "Key" msgstr "Kľúč" -#: templates/debug_toolbar/panels/headers.html:9 -#: templates/debug_toolbar/panels/headers.html:28 -#: templates/debug_toolbar/panels/headers.html:49 -#: templates/debug_toolbar/panels/request.html:33 -#: templates/debug_toolbar/panels/request.html:59 -#: templates/debug_toolbar/panels/request.html:85 -#: templates/debug_toolbar/panels/request.html:110 -#: templates/debug_toolbar/panels/settings.html:6 -#: templates/debug_toolbar/panels/timer.html:11 +#: templates/debug_toolbar/panels/headers.html:9 templates/debug_toolbar/panels/headers.html:28 templates/debug_toolbar/panels/headers.html:49 +#: templates/debug_toolbar/panels/request.html:33 templates/debug_toolbar/panels/request.html:59 templates/debug_toolbar/panels/request.html:85 +#: templates/debug_toolbar/panels/request.html:110 templates/debug_toolbar/panels/settings.html:6 templates/debug_toolbar/panels/timer.html:11 msgid "Value" msgstr "Hodnota" @@ -372,9 +363,7 @@ msgid "WSGI environ" msgstr "WSGI prostredie" #: templates/debug_toolbar/panels/headers.html:43 -msgid "" -"Since the WSGI environ inherits the environment of the server, only a " -"significant subset is shown below." +msgid "Since the WSGI environ inherits the environment of the server, only a significant subset is shown below." msgstr "Keďže WSGI prostredie dedí z prostredia servera, je nižšie zobrazená iba významná podmnožina." #: templates/debug_toolbar/panels/logging.html:6 @@ -389,8 +378,7 @@ msgstr "Kanál" msgid "Message" msgstr "Správa" -#: templates/debug_toolbar/panels/logging.html:10 -#: templates/debug_toolbar/panels/staticfiles.html:45 +#: templates/debug_toolbar/panels/logging.html:10 templates/debug_toolbar/panels/staticfiles.html:45 msgid "Location" msgstr "Poloha" @@ -406,8 +394,7 @@ msgstr "Volanie" msgid "CumTime" msgstr "CumTime" -#: templates/debug_toolbar/panels/profiling.html:7 -#: templates/debug_toolbar/panels/profiling.html:9 +#: templates/debug_toolbar/panels/profiling.html:7 templates/debug_toolbar/panels/profiling.html:9 msgid "Per" msgstr "Za" @@ -435,9 +422,7 @@ msgstr "URL meno" msgid "Cookies" msgstr "Cookies" -#: templates/debug_toolbar/panels/request.html:32 -#: templates/debug_toolbar/panels/request.html:58 -#: templates/debug_toolbar/panels/request.html:84 +#: templates/debug_toolbar/panels/request.html:32 templates/debug_toolbar/panels/request.html:58 templates/debug_toolbar/panels/request.html:84 #: templates/debug_toolbar/panels/request.html:109 msgid "Variable" msgstr "Premenná" @@ -491,15 +476,15 @@ msgstr "Príjemcovia" msgid "%(num)s query" msgid_plural "%(num)s queries" msgstr[0] "%(num)s dopyt" -msgstr[1] "%(num)s dopytov" -msgstr[2] "%(num)s dopytov" +msgstr[1] "%(num)s dopyty" +msgstr[2] "%(num)s dopytu" +msgstr[3] "%(num)s dopytov" #: templates/debug_toolbar/panels/sql.html:18 msgid "Query" msgstr "Dopyt" -#: templates/debug_toolbar/panels/sql.html:19 -#: templates/debug_toolbar/panels/timer.html:36 +#: templates/debug_toolbar/panels/sql.html:19 templates/debug_toolbar/panels/timer.html:36 msgid "Timeline" msgstr "Časová os" @@ -527,9 +512,7 @@ msgstr "(neznámy)" msgid "No SQL queries were recorded during this request." msgstr "V priebehu tejto požiadavky neboli zaznamenané žiadne SQL dopyty." -#: templates/debug_toolbar/panels/sql_explain.html:3 -#: templates/debug_toolbar/panels/sql_profile.html:3 -#: templates/debug_toolbar/panels/sql_select.html:3 +#: templates/debug_toolbar/panels/sql_explain.html:3 templates/debug_toolbar/panels/sql_profile.html:3 templates/debug_toolbar/panels/sql_select.html:3 #: templates/debug_toolbar/panels/template_source.html:3 msgid "Back" msgstr "Späť" @@ -538,15 +521,11 @@ msgstr "Späť" msgid "SQL explained" msgstr "SQL vysvetlené" -#: templates/debug_toolbar/panels/sql_explain.html:9 -#: templates/debug_toolbar/panels/sql_profile.html:10 -#: templates/debug_toolbar/panels/sql_select.html:9 +#: templates/debug_toolbar/panels/sql_explain.html:9 templates/debug_toolbar/panels/sql_profile.html:10 templates/debug_toolbar/panels/sql_select.html:9 msgid "Executed SQL" msgstr "Vykonané SQL" -#: templates/debug_toolbar/panels/sql_explain.html:13 -#: templates/debug_toolbar/panels/sql_profile.html:14 -#: templates/debug_toolbar/panels/sql_select.html:13 +#: templates/debug_toolbar/panels/sql_explain.html:13 templates/debug_toolbar/panels/sql_profile.html:14 templates/debug_toolbar/panels/sql_select.html:13 msgid "Database" msgstr "Databáza" @@ -572,18 +551,15 @@ msgid_plural "Static file paths" msgstr[0] "Cesta k statickému súboru" msgstr[1] "Cesty k statickým súborom" msgstr[2] "Cesty k statickým súborom" +msgstr[3] "Ciest k statickým súborom" #: templates/debug_toolbar/panels/staticfiles.html:8 #, python-format msgid "(prefix %(prefix)s)" msgstr "(prefix %(prefix)s)" -#: templates/debug_toolbar/panels/staticfiles.html:12 -#: templates/debug_toolbar/panels/staticfiles.html:23 -#: templates/debug_toolbar/panels/staticfiles.html:35 -#: templates/debug_toolbar/panels/templates.html:10 -#: templates/debug_toolbar/panels/templates.html:28 -#: templates/debug_toolbar/panels/templates.html:43 +#: templates/debug_toolbar/panels/staticfiles.html:12 templates/debug_toolbar/panels/staticfiles.html:23 templates/debug_toolbar/panels/staticfiles.html:35 +#: templates/debug_toolbar/panels/templates.html:10 templates/debug_toolbar/panels/templates.html:28 templates/debug_toolbar/panels/templates.html:43 msgid "None" msgstr "Žiadny" @@ -593,21 +569,24 @@ msgid_plural "Static file apps" msgstr[0] "Aplikácia pre statické súbory" msgstr[1] "Aplikácie pre statické súbory" msgstr[2] "Aplikácie pre statické súbory" +msgstr[3] "Aplikácií pre statické súbory" #: templates/debug_toolbar/panels/staticfiles.html:26 msgid "Static file" msgid_plural "Static files" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "Statické súbory" +msgstr[0] "Statický súbor" +msgstr[1] "Statické súbory" +msgstr[2] "Statického súbora" +msgstr[3] "Statických súborov" #: templates/debug_toolbar/panels/staticfiles.html:40 #, python-format msgid "%(payload_count)s file" msgid_plural "%(payload_count)s files" msgstr[0] "%(payload_count)s súbor" -msgstr[1] "%(payload_count)s súborov" -msgstr[2] "%(payload_count)s súborov" +msgstr[1] "%(payload_count)s súbory" +msgstr[2] "%(payload_count)s súbora" +msgstr[3] "%(payload_count)s súborov" #: templates/debug_toolbar/panels/staticfiles.html:44 msgid "Path" @@ -621,18 +600,19 @@ msgstr "Zdrojový kód šablóny:" msgid "Template path" msgid_plural "Template paths" msgstr[0] "Cesta k šablóne" -msgstr[1] "Cesta k šablóne" -msgstr[2] "Cesta k šablóne" +msgstr[1] "Cesty k šablóne" +msgstr[2] "Cesty k šablóne" +msgstr[3] "Ciest k šablóne" #: templates/debug_toolbar/panels/templates.html:13 msgid "Template" msgid_plural "Templates" msgstr[0] "Šablóna" -msgstr[1] "Šablóna" -msgstr[2] "Šablóna" +msgstr[1] "Šablóny" +msgstr[2] "Šablóny" +msgstr[3] "Šablón" -#: templates/debug_toolbar/panels/templates.html:21 -#: templates/debug_toolbar/panels/templates.html:37 +#: templates/debug_toolbar/panels/templates.html:21 templates/debug_toolbar/panels/templates.html:37 msgid "Toggle context" msgstr "Prepnúť kontext" @@ -640,8 +620,9 @@ msgstr "Prepnúť kontext" msgid "Context processor" msgid_plural "Context processors" msgstr[0] "Spracovateľ kontextu" -msgstr[1] "Spracovateľ kontextu" -msgstr[2] "Spracovateľ kontextu" +msgstr[1] "Spracovatelia kontextu" +msgstr[2] "Spracovateľa kontextu" +msgstr[3] "Spracovateľov kontextu" #: templates/debug_toolbar/panels/timer.html:2 msgid "Resource usage" diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index ec3445c1e..8fd433c63 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -107,6 +107,10 @@ def content(self): def scripts(self): """ Scripts used by the HTML content of the panel when it's displayed. + + When a panel is rendered on the frontend, the ``djdt.panel.render`` + JavaScript event will be dispatched. The scripts can listen for + this event to support dynamic functionality. """ return [] diff --git a/debug_toolbar/panels/history/panel.py b/debug_toolbar/panels/history/panel.py index 4494bbfcd..541c59136 100644 --- a/debug_toolbar/panels/history/panel.py +++ b/debug_toolbar/panels/history/panel.py @@ -15,12 +15,18 @@ class HistoryPanel(Panel): - """ A panel to display History """ + """A panel to display History""" title = _("History") nav_title = _("History") template = "debug_toolbar/panels/history.html" + @property + def enabled(self): + # Do not show the history panel if the panels are rendered on request + # rather than loaded via ajax. + return super().enabled and not self.toolbar.should_render_panels() + @property def is_historical(self): """The HistoryPanel should not be included in the historical panels.""" @@ -62,6 +68,7 @@ def generate_stats(self, request, response): { "request_url": request.get_full_path(), "request_method": request.method, + "status_code": response.status_code, "data": data, "time": timezone.now(), } diff --git a/debug_toolbar/panels/history/views.py b/debug_toolbar/panels/history/views.py index b4cf8c835..10b4dcc1a 100644 --- a/debug_toolbar/panels/history/views.py +++ b/debug_toolbar/panels/history/views.py @@ -2,6 +2,7 @@ from django.template.loader import render_to_string from debug_toolbar.decorators import require_show_toolbar, signed_data_view +from debug_toolbar.forms import SignedDataForm from debug_toolbar.panels.history.forms import HistoryStoreForm from debug_toolbar.toolbar import DebugToolbar @@ -16,6 +17,10 @@ def history_sidebar(request, verified_data): store_id = form.cleaned_data["store_id"] toolbar = DebugToolbar.fetch(store_id) context = {} + if toolbar is None: + # When the store_id has been popped already due to + # RESULTS_CACHE_SIZE + return JsonResponse(context) for panel in toolbar.panels: if not panel.is_historical: continue @@ -40,7 +45,8 @@ def history_refresh(request, verified_data): if form.is_valid(): requests = [] - for id, toolbar in reversed(DebugToolbar._store.items()): + # Convert to list to handle mutations happenening in parallel + for id, toolbar in list(DebugToolbar._store.items())[::-1]: requests.append( { "id": id, @@ -50,7 +56,11 @@ def history_refresh(request, verified_data): "id": id, "store_context": { "toolbar": toolbar, - "form": HistoryStoreForm(initial={"store_id": id}), + "form": SignedDataForm( + initial=HistoryStoreForm( + initial={"store_id": id} + ).initial + ), }, }, ), diff --git a/debug_toolbar/panels/logging.py b/debug_toolbar/panels/logging.py index f296fc882..a7252c2bb 100644 --- a/debug_toolbar/panels/logging.py +++ b/debug_toolbar/panels/logging.py @@ -78,6 +78,6 @@ def process_request(self, request): def generate_stats(self, request, response): records = collector.get_collection() - self._records[threading.currentThread()] = records + self._records[threading.current_thread()] = records collector.clear_collection() self.record_stats({"records": records}) diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py index 75366802c..2ed691344 100644 --- a/debug_toolbar/panels/sql/tracking.py +++ b/debug_toolbar/panels/sql/tracking.py @@ -55,9 +55,12 @@ def cursor(*args, **kwargs): ) def chunked_cursor(*args, **kwargs): - return state.Wrapper( - connection._djdt_chunked_cursor(*args, **kwargs), connection, panel - ) + # prevent double wrapping + # solves https://github.com/jazzband/django-debug-toolbar/issues/1239 + cursor = connection._djdt_chunked_cursor(*args, **kwargs) + if not isinstance(cursor, BaseCursorWrapper): + return state.Wrapper(cursor, connection, panel) + return cursor connection.cursor = cursor connection.chunked_cursor = chunked_cursor @@ -71,7 +74,11 @@ def unwrap_cursor(connection): del connection.chunked_cursor -class ExceptionCursorWrapper: +class BaseCursorWrapper: + pass + + +class ExceptionCursorWrapper(BaseCursorWrapper): """ Wraps a cursor and raises an exception on any operation. Used in Templates panel. @@ -84,7 +91,7 @@ def __getattr__(self, attr): raise SQLQueryTriggered() -class NormalCursorWrapper: +class NormalCursorWrapper(BaseCursorWrapper): """ Wraps a cursor and logs queries. """ diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py index 9a15c0f28..ef6af5d3e 100644 --- a/debug_toolbar/panels/staticfiles.py +++ b/debug_toolbar/panels/staticfiles.py @@ -118,7 +118,7 @@ def process_request(self, request): def generate_stats(self, request, response): used_paths = collector.get_collection() - self._paths[threading.currentThread()] = used_paths + self._paths[threading.current_thread()] = used_paths self.record_stats( { diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index 6bbfd3d73..13fbbed0a 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -198,7 +198,7 @@ #djDebug #djDebugToolbarHandle { position: fixed; - transform: rotate(-90deg); + transform: translateY(-100%) rotate(-90deg); transform-origin: right bottom; background-color: #fff; border: 1px solid #111; diff --git a/debug_toolbar/static/debug_toolbar/js/history.js b/debug_toolbar/static/debug_toolbar/js/history.js index e20c85438..cc14b2e4f 100644 --- a/debug_toolbar/static/debug_toolbar/js/history.js +++ b/debug_toolbar/static/debug_toolbar/js/history.js @@ -7,21 +7,30 @@ $$.on(djDebug, "click", ".switchHistory", function (event) { const newStoreId = this.dataset.storeId; const tbody = this.closest("tbody"); - tbody - .querySelector(".djdt-highlighted") - .classList.remove("djdt-highlighted"); + const highlighted = tbody.querySelector(".djdt-highlighted"); + if (highlighted) { + highlighted.classList.remove("djdt-highlighted"); + } this.closest("tr").classList.add("djdt-highlighted"); ajaxForm(this).then(function (data) { djDebug.setAttribute("data-store-id", newStoreId); - Object.keys(data).forEach(function (panelId) { - const panel = document.getElementById(panelId); - if (panel) { - panel.outerHTML = data[panelId].content; - document.getElementById("djdt-" + panelId).outerHTML = - data[panelId].button; - } - }); + // Check if response is empty, it could be due to an expired store_id. + if (Object.keys(data).length === 0) { + const container = document.getElementById("djdtHistoryRequests"); + container.querySelector( + 'button[data-store-id="' + newStoreId + '"]' + ).innerHTML = "Switch [EXPIRED]"; + } else { + Object.keys(data).forEach(function (panelId) { + const panel = document.getElementById(panelId); + if (panel) { + panel.outerHTML = data[panelId].content; + document.getElementById("djdt-" + panelId).outerHTML = + data[panelId].button; + } + }); + } }); }); @@ -29,12 +38,14 @@ $$.on(djDebug, "click", ".refreshHistory", function (event) { event.preventDefault(); const container = document.getElementById("djdtHistoryRequests"); ajaxForm(this).then(function (data) { + // Remove existing rows first then re-populate with new data + container + .querySelectorAll("tr[data-store-id]") + .forEach(function (node) { + node.remove(); + }); data.requests.forEach(function (request) { - if ( - !container.querySelector('[data-store-id="' + request.id + '"]') - ) { - container.innerHTML = request.content + container.innerHTML; - } + container.innerHTML = request.content + container.innerHTML; }); }); }); diff --git a/debug_toolbar/static/debug_toolbar/js/timer.js b/debug_toolbar/static/debug_toolbar/js/timer.js index 1d4ac19d8..70d3fe5a2 100644 --- a/debug_toolbar/static/debug_toolbar/js/timer.js +++ b/debug_toolbar/static/debug_toolbar/js/timer.js @@ -1,59 +1,75 @@ -const timingOffset = performance.timing.navigationStart, - timingEnd = performance.timing.loadEventEnd, - totalTime = timingEnd - timingOffset; -function getLeft(stat) { - return ((performance.timing[stat] - timingOffset) / totalTime) * 100.0; -} -function getCSSWidth(stat, endStat) { - let width = - ((performance.timing[endStat] - performance.timing[stat]) / totalTime) * - 100.0; - // Calculate relative percent (same as sql panel logic) - width = (100.0 * width) / (100.0 - getLeft(stat)); - return width < 1 ? "2px" : width + "%"; -} -function addRow(tbody, stat, endStat) { - const row = document.createElement("tr"); - if (endStat) { - // Render a start through end bar - row.innerHTML = - "" + - stat.replace("Start", "") + - "" + - '' + - "" + - (performance.timing[stat] - timingOffset) + - " (+" + - (performance.timing[endStat] - performance.timing[stat]) + - ")"; - row.querySelector("rect").setAttribute( - "width", - getCSSWidth(stat, endStat) - ); - } else { - // Render a point in time - row.innerHTML = - "" + - stat + - "" + - '' + - "" + - (performance.timing[stat] - timingOffset) + - ""; - row.querySelector("rect").setAttribute("width", 2); +import { $$ } from "./utils.js"; + +function insertBrowserTiming() { + console.log(["inserted"]); + const timingOffset = performance.timing.navigationStart, + timingEnd = performance.timing.loadEventEnd, + totalTime = timingEnd - timingOffset; + function getLeft(stat) { + return ((performance.timing[stat] - timingOffset) / totalTime) * 100.0; + } + function getCSSWidth(stat, endStat) { + let width = + ((performance.timing[endStat] - performance.timing[stat]) / + totalTime) * + 100.0; + // Calculate relative percent (same as sql panel logic) + width = (100.0 * width) / (100.0 - getLeft(stat)); + return width < 1 ? "2px" : width + "%"; + } + function addRow(tbody, stat, endStat) { + const row = document.createElement("tr"); + if (endStat) { + // Render a start through end bar + row.innerHTML = + "" + + stat.replace("Start", "") + + "" + + '' + + "" + + (performance.timing[stat] - timingOffset) + + " (+" + + (performance.timing[endStat] - performance.timing[stat]) + + ")"; + row.querySelector("rect").setAttribute( + "width", + getCSSWidth(stat, endStat) + ); + } else { + // Render a point in time + row.innerHTML = + "" + + stat + + "" + + '' + + "" + + (performance.timing[stat] - timingOffset) + + ""; + row.querySelector("rect").setAttribute("width", 2); + } + row.querySelector("rect").setAttribute("x", getLeft(stat)); + tbody.appendChild(row); + } + + const browserTiming = document.getElementById("djDebugBrowserTiming"); + // Determine if the browser timing section has already been rendered. + if (browserTiming.classList.contains("djdt-hidden")) { + const tbody = document.getElementById("djDebugBrowserTimingTableBody"); + // This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param) + addRow(tbody, "domainLookupStart", "domainLookupEnd"); + addRow(tbody, "connectStart", "connectEnd"); + addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd + addRow(tbody, "responseStart", "responseEnd"); + addRow(tbody, "domLoading", "domComplete"); // Spans the events below + addRow(tbody, "domInteractive"); + addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); + addRow(tbody, "loadEventStart", "loadEventEnd"); + browserTiming.classList.remove("djdt-hidden"); } - row.querySelector("rect").setAttribute("x", getLeft(stat)); - tbody.appendChild(row); } -const tbody = document.getElementById("djDebugBrowserTimingTableBody"); -// This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param) -addRow(tbody, "domainLookupStart", "domainLookupEnd"); -addRow(tbody, "connectStart", "connectEnd"); -addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd -addRow(tbody, "responseStart", "responseEnd"); -addRow(tbody, "domLoading", "domComplete"); // Spans the events below -addRow(tbody, "domInteractive"); -addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); -addRow(tbody, "loadEventStart", "loadEventEnd"); -document.getElementById("djDebugBrowserTiming").classList.remove("djdt-hidden"); +const djDebug = document.getElementById("djDebug"); +// Insert the browser timing now since it's possible for this +// script to miss the initial panel load event. +insertBrowserTiming(); +$$.onPanelRender(djDebug, "TimerPanel", insertBrowserTiming); diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index d739cbdb3..c17ee3ea2 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -20,7 +20,8 @@ const djdt = { if (!this.className) { return; } - const current = document.getElementById(this.className); + const panelId = this.className; + const current = document.getElementById(panelId); if ($$.visible(current)) { djdt.hide_panels(); } else { @@ -39,13 +40,24 @@ const djdt = { window.location ); url.searchParams.append("store_id", store_id); - url.searchParams.append("panel_id", this.className); + url.searchParams.append("panel_id", panelId); ajax(url).then(function (data) { inner.previousElementSibling.remove(); // Remove AJAX loader inner.innerHTML = data.content; $$.executeScripts(data.scripts); $$.applyStyles(inner); + djDebug.dispatchEvent( + new CustomEvent("djdt.panel.render", { + detail: { panelId: panelId }, + }) + ); }); + } else { + djDebug.dispatchEvent( + new CustomEvent("djdt.panel.render", { + detail: { panelId: panelId }, + }) + ); } } } @@ -178,6 +190,7 @@ const djdt = { requestAnimationFrame(function () { djdt.handleDragged = false; }); + djdt.ensure_handle_visibility(); } }); const show = @@ -198,6 +211,15 @@ const djdt = { e.classList.remove("djdt-active"); }); }, + ensure_handle_visibility() { + const handle = document.getElementById("djDebugToolbarHandle"); + // set handle position + const handleTop = Math.min( + localStorage.getItem("djdt.top") || 0, + window.innerHeight - handle.offsetWidth + ); + handle.style.top = handleTop + "px"; + }, hide_toolbar() { djdt.hide_panels(); @@ -205,16 +227,8 @@ const djdt = { const handle = document.getElementById("djDebugToolbarHandle"); $$.show(handle); - // set handle position - let handleTop = localStorage.getItem("djdt.top"); - if (handleTop) { - handleTop = Math.min( - handleTop, - window.innerHeight - handle.offsetHeight - ); - handle.style.top = handleTop + "px"; - } - + djdt.ensure_handle_visibility(); + window.addEventListener("resize", djdt.ensure_handle_visibility); document.removeEventListener("keydown", onKeyDown); localStorage.setItem("djdt.show", "false"); @@ -237,6 +251,7 @@ const djdt = { $$.hide(document.getElementById("djDebugToolbarHandle")); $$.show(document.getElementById("djDebugToolbar")); localStorage.setItem("djdt.show", "true"); + window.removeEventListener("resize", djdt.ensure_handle_visibility); }, cookie: { get(key) { diff --git a/debug_toolbar/static/debug_toolbar/js/utils.js b/debug_toolbar/static/debug_toolbar/js/utils.js index 4683b319f..da810aad0 100644 --- a/debug_toolbar/static/debug_toolbar/js/utils.js +++ b/debug_toolbar/static/debug_toolbar/js/utils.js @@ -7,6 +7,21 @@ const $$ = { } }); }, + onPanelRender(root, panelId, fn) { + /* + This is a helper function to attach a handler for a `djdt.panel.render` + event of a specific panel. + + root: The container element that the listener should be attached to. + panelId: The Id of the panel. + fn: A function to execute when the event is triggered. + */ + root.addEventListener("djdt.panel.render", function (event) { + if (event.detail.panelId === panelId) { + fn.call(event); + } + }); + }, show(element) { element.classList.remove("djdt-hidden"); }, diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index 78b9b7fe2..7abc5476f 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -7,7 +7,10 @@ {% endblock %}
diff --git a/debug_toolbar/templates/debug_toolbar/includes/panel_content.html b/debug_toolbar/templates/debug_toolbar/includes/panel_content.html index 2c1a1b195..8c2e446b1 100644 --- a/debug_toolbar/templates/debug_toolbar/includes/panel_content.html +++ b/debug_toolbar/templates/debug_toolbar/includes/panel_content.html @@ -7,11 +7,11 @@

{{ panel.title }}

- {% if toolbar.store_id %} + {% if toolbar.should_render_panels %} +
{{ panel.content }}
+ {% else %}
- {% else %} -
{{ panel.content }}
{% endif %}
diff --git a/debug_toolbar/templates/debug_toolbar/panels/history.html b/debug_toolbar/templates/debug_toolbar/panels/history.html index f5e967a17..84c6cb5bd 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/history.html +++ b/debug_toolbar/templates/debug_toolbar/panels/history.html @@ -10,6 +10,7 @@ {% trans "Method" %} {% trans "Path" %} {% trans "Request Variables" %} + {% trans "Status" %} {% trans "Action" %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/history_tr.html b/debug_toolbar/templates/debug_toolbar/panels/history_tr.html index 9ce984396..31793472a 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/history_tr.html +++ b/debug_toolbar/templates/debug_toolbar/panels/history_tr.html @@ -38,6 +38,9 @@ + +

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

+
{{ store_context.form }} diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py index fd82d62e2..cb886c407 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -79,6 +79,10 @@ def render_toolbar(self): raise def should_render_panels(self): + """Determine whether the panels should be rendered during the request + + If False, the panels will be loaded via Ajax. + """ render_panels = self.config["RENDER_PANELS"] if render_panels is None: render_panels = self.request.META["wsgi.multiprocess"] @@ -142,7 +146,9 @@ def is_toolbar_request(cls, request): # The primary caller of this function is in the middleware which may # not have resolver_match set. try: - resolver_match = request.resolver_match or resolve(request.path) + resolver_match = request.resolver_match or resolve( + request.path, getattr(request, "urlconf", None) + ) except Resolver404: return False return resolver_match.namespaces and resolver_match.namespaces[-1] == app_name diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py index cc5d74477..f8fb68538 100644 --- a/debug_toolbar/utils.py +++ b/debug_toolbar/utils.py @@ -135,7 +135,14 @@ def get_template_context(node, context, context_lines=3): def get_template_source_from_exception_info(node, context): - exception_info = context.template.get_exception_info(Exception("DDT"), node.token) + if context.template.origin == node.origin: + exception_info = context.template.get_exception_info( + Exception("DDT"), node.token + ) + else: + exception_info = context.render_context.template.get_exception_info( + Exception("DDT"), node.token + ) line = exception_info["line"] source_lines = exception_info["source_lines"] name = exception_info["name"] @@ -252,14 +259,14 @@ def get_collection(self, thread=None): is provided, returns a list for the current thread. """ if thread is None: - thread = threading.currentThread() + thread = threading.current_thread() if thread not in self.collections: self.collections[thread] = [] return self.collections[thread] def clear_collection(self, thread=None): if thread is None: - thread = threading.currentThread() + thread = threading.current_thread() if thread in self.collections: del self.collections[thread] diff --git a/docs/changes.rst b/docs/changes.rst index 19aaab12d..1a322752e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,19 @@ Next version ------------ +3.2.2 (2021-08-14) +------------------ + +* Ensured that the handle stays within bounds when resizing the window. +* Disabled ``HistoryPanel`` when ``RENDER_PANELS`` is ``True`` + or if ``RENDER_PANELS`` is ``None`` and the WSGI container is + running with multiple processes. +* Fixed ``RENDER_PANELS`` functionality so that when ``True`` panels are + rendered during the request and not loaded asynchronously. +* HistoryPanel now shows status codes of responses. +* Support ``request.urlconf`` override when checking for toolbar requests. + + 3.2.1 (2021-04-14) ------------------ @@ -16,8 +29,13 @@ Next version * Added ``PRETTIFY_SQL`` configuration option to support controlling SQL token grouping. By default it's set to True. When set to False, a performance improvement can be seen by the SQL panel. -* Fixed issue with toolbar expecting URL paths to start with `/__debug__/` - while the documentation indicates it's not required. +* Added a JavaScript event when a panel loads of the format + ``djdt.panel.[PanelId]`` where PanelId is the ``panel_id`` property + of the panel's Python class. Listening for this event corrects the bug + in the Timer Panel in which it didn't insert the browser timings + after switching requests in the History Panel. +* Fixed issue with the toolbar expecting URL paths to start with + ``/__debug__/`` while the documentation indicates it's not required. 3.2 (2020-12-03) ---------------- diff --git a/docs/conf.py b/docs/conf.py index f3afd1888..1fdad323e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ copyright = copyright.format(datetime.date.today().year) # The full version, including alpha/beta/rc tags -release = "3.2.1" +release = "3.2.2" # -- General configuration --------------------------------------------------- diff --git a/docs/configuration.rst b/docs/configuration.rst index 92b493000..0d7cd87c4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -66,19 +66,24 @@ Toolbar options The toolbar searches for this string in the HTML and inserts itself just before. +.. _RENDER_PANELS: + * ``RENDER_PANELS`` Default: ``None`` If set to ``False``, the debug toolbar will keep the contents of panels in - memory on the server and load them on demand. If set to ``True``, it will - render panels inside every page. This may slow down page rendering but it's + memory on the server and load them on demand. + + If set to ``True``, it will disable ``HistoryPanel`` and render panels + inside every page. This may slow down page rendering but it's required on multi-process servers, for example if you deploy the toolbar in production (which isn't recommended). The default value of ``None`` tells the toolbar to automatically do the right thing depending on whether the WSGI container runs multiple processes. - This setting allows you to force a different behavior if needed. + This setting allows you to force a different behavior if needed. If the + WSGI container runs multiple processes, it will disable ``HistoryPanel``. * ``RESULTS_CACHE_SIZE`` diff --git a/docs/installation.rst b/docs/installation.rst index 0c69e09af..4eff7fc89 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -147,3 +147,11 @@ And for Apache: .. _JavaScript module: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules .. _CORS errors: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin .. _Access-Control-Allow-Origin header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + +Django Channels & Async +^^^^^^^^^^^^^^^^^^^^^^^ + +The Debug Toolbar currently doesn't support Django Channels or async projects. +If you are using Django channels are having issues getting panels to load, +please review the documentation for the configuration option +:ref:`RENDER_PANELS `. diff --git a/docs/panels.rst b/docs/panels.rst index c21e90801..a836054ed 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -17,6 +17,11 @@ History This panel shows the history of requests made and allows switching to a past snapshot of the toolbar to view that request's stats. +.. caution:: + If :ref:`RENDER_PANELS ` configuration option is set to + ``True`` or if the server runs with multiple processes, the History Panel + will be disabled. + Version ~~~~~~~ @@ -184,9 +189,9 @@ URL: https://github.com/danyi1212/django-windowsauth Path: ``windows_auth.panels.LDAPPanel`` -LDAP Operations performed during the request, including timing, request and response messages, +LDAP Operations performed during the request, including timing, request and response messages, the entries received, write changes list, stack-tracing and error debugging. -This panel also shows connection usage metrics when it is collected. +This panel also shows connection usage metrics when it is collected. `Check out the docs `_. Line Profiler @@ -402,3 +407,32 @@ common methods available. .. js:function:: djdt.show_toolbar Shows the toolbar. + +Events +^^^^^^ + +.. js:attribute:: djdt.panel.render + + This is an event raised when a panel is rendered. It has the property + ``detail.panelId`` which identifies which panel has been loaded. This + event can be useful when creating custom scripts to process the HTML + further. + + An example of this for the ``CustomPanel`` would be: + +.. code-block:: javascript + + import { $$ } from "./utils.js"; + function addCustomMetrics() { + // Logic to process/add custom metrics here. + + // Be sure to cover the case of this function being called twice + // due to file being loaded asynchronously. + } + const djDebug = document.getElementById("djDebug"); + $$.onPanelRender(djDebug, "CustomPanel", addCustomMetrics); + // Since a panel's scripts are loaded asynchronously, it's possible that + // the above statement would occur after the djdt.panel.render event has + // been raised. To account for that, the rendering function should be + // called here as well. + addCustomMetrics(); diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index ede7915a1..7250ed750 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -37,3 +37,4 @@ unhashable uWSGI validator Werkzeug +async diff --git a/example/django-debug-toolbar.png b/example/django-debug-toolbar.png index 762411772..3ad4a4af1 100644 Binary files a/example/django-debug-toolbar.png and b/example/django-debug-toolbar.png differ diff --git a/example/screenshot.py b/example/screenshot.py index 0d0ae8dc5..8c1135eb2 100644 --- a/example/screenshot.py +++ b/example/screenshot.py @@ -3,6 +3,7 @@ import os import signal import subprocess +from time import sleep from selenium.webdriver.common.keys import Keys from selenium.webdriver.support import expected_conditions as EC @@ -33,7 +34,10 @@ def create_webdriver(browser, headless): def example_server(): - return subprocess.Popen(["make", "example"]) + proc = subprocess.Popen(["make", "example"]) + # `make example` runs a few things before runserver. + sleep(2) + return proc def set_viewport_size(selenium, width, height): @@ -67,12 +71,15 @@ def main(): submit_form(selenium, {"username": os.environ["USER"], "password": "p"}) selenium.get("http://localhost:8000/admin/auth/user/") - # Close the admin sidebar. - el = selenium.find_element_by_id("toggle-nav-sidebar") - el.click() + # Check if SQL Panel is already visible: + sql_panel = selenium.find_element_by_id("djdt-SQLPanel") + if not sql_panel: + # Open the admin sidebar. + el = selenium.find_element_by_id("djDebugToolbarHandle") + el.click() + sql_panel = selenium.find_element_by_id("djdt-SQLPanel") # Open the SQL panel. - el = selenium.find_element_by_id("djdt-SQLPanel") - el.click() + sql_panel.click() selenium.save_screenshot(args.outfile) finally: diff --git a/setup.cfg b/setup.cfg index d1df17267..912fe1186 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,9 @@ [metadata] name = django-debug-toolbar -version = 3.2.1 +version = 3.2.2 description = A configurable set of panels that display various debug information about the current request/response. long_description = file: README.rst +long_description_content_type = text/x-rst author = Rob Hudson author_email = rob@cogit8.org url = https://github.com/jazzband/django-debug-toolbar @@ -14,8 +15,8 @@ classifiers = Environment :: Web Environment Framework :: Django Framework :: Django :: 2.2 - Framework :: Django :: 3.0 Framework :: Django :: 3.1 + Framework :: Django :: 3.2 Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent diff --git a/tests/panels/test_history.py b/tests/panels/test_history.py index 03657a374..49e3bd0fa 100644 --- a/tests/panels/test_history.py +++ b/tests/panels/test_history.py @@ -1,3 +1,5 @@ +import html + from django.test import RequestFactory, override_settings from django.urls import resolve, reverse @@ -64,6 +66,21 @@ def test_urls(self): @override_settings(DEBUG=True) class HistoryViewsTestCase(IntegrationTestCase): + PANEL_KEYS = { + "VersionsPanel", + "TimerPanel", + "SettingsPanel", + "HeadersPanel", + "RequestPanel", + "SQLPanel", + "StaticFilesPanel", + "TemplatesPanel", + "CachePanel", + "SignalsPanel", + "LoggingPanel", + "ProfilingPanel", + } + def test_history_panel_integration_content(self): """Verify the history panel's content renders properly..""" self.assertEqual(len(DebugToolbar._store), 0) @@ -88,26 +105,45 @@ def test_history_sidebar_invalid(self): def test_history_sidebar(self): """Validate the history sidebar view.""" self.client.get("/json_view/") - store_id = list(DebugToolbar._store.keys())[0] + store_id = list(DebugToolbar._store)[0] + data = {"signed": SignedDataForm.sign({"store_id": store_id})} + response = self.client.get(reverse("djdt:history_sidebar"), data=data) + self.assertEqual(response.status_code, 200) + self.assertEqual( + set(response.json()), + self.PANEL_KEYS, + ) + + @override_settings( + DEBUG_TOOLBAR_CONFIG={"RESULTS_CACHE_SIZE": 1, "RENDER_PANELS": False} + ) + def test_history_sidebar_expired_store_id(self): + """Validate the history sidebar view.""" + self.client.get("/json_view/") + store_id = list(DebugToolbar._store)[0] + data = {"signed": SignedDataForm.sign({"store_id": store_id})} + response = self.client.get(reverse("djdt:history_sidebar"), data=data) + self.assertEqual(response.status_code, 200) + self.assertEqual( + set(response.json()), + self.PANEL_KEYS, + ) + self.client.get("/json_view/") + + # Querying old store_id should return in empty response data = {"signed": SignedDataForm.sign({"store_id": store_id})} response = self.client.get(reverse("djdt:history_sidebar"), data=data) self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {}) + + # Querying with latest store_id + latest_store_id = list(DebugToolbar._store)[0] + data = {"signed": SignedDataForm.sign({"store_id": latest_store_id})} + response = self.client.get(reverse("djdt:history_sidebar"), data=data) + self.assertEqual(response.status_code, 200) self.assertEqual( - set(response.json().keys()), - { - "VersionsPanel", - "TimerPanel", - "SettingsPanel", - "HeadersPanel", - "RequestPanel", - "SQLPanel", - "StaticFilesPanel", - "TemplatesPanel", - "CachePanel", - "SignalsPanel", - "LoggingPanel", - "ProfilingPanel", - }, + set(response.json()), + self.PANEL_KEYS, ) def test_history_refresh_invalid_signature(self): @@ -128,5 +164,10 @@ def test_history_refresh(self): self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(len(data["requests"]), 1) + + store_id = list(DebugToolbar._store)[0] + signature = SignedDataForm.sign({"store_id": store_id}) + self.assertIn(html.escape(signature), data["requests"][0]["content"]) + for val in ["foo", "bar"]: self.assertIn(val, data["requests"][0]["content"]) diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py index 9ed2b1a6e..08535a79e 100644 --- a/tests/panels/test_sql.py +++ b/tests/panels/test_sql.py @@ -1,5 +1,7 @@ import datetime +import os import unittest +from unittest.mock import patch import django from django.contrib.auth.models import User @@ -9,6 +11,7 @@ from django.shortcuts import render from django.test.utils import override_settings +import debug_toolbar.panels.sql.tracking as sql_tracking from debug_toolbar import settings as dt_settings from ..base import BaseTestCase @@ -60,6 +63,20 @@ def test_recording_chunked_cursor(self): # ensure query was logged self.assertEqual(len(self.panel._queries), 1) + @patch("debug_toolbar.panels.sql.tracking.state", wraps=sql_tracking.state) + def test_cursor_wrapper_singleton(self, mock_state): + list(User.objects.all()) + + # ensure that cursor wrapping is applied only once + self.assertEqual(mock_state.Wrapper.call_count, 1) + + @patch("debug_toolbar.panels.sql.tracking.state", wraps=sql_tracking.state) + def test_chunked_cursor_wrapper_singleton(self, mock_state): + list(User.objects.all().iterator()) + + # ensure that cursor wrapping is applied only once + self.assertEqual(mock_state.Wrapper.call_count, 1) + def test_generate_server_timing(self): self.assertEqual(len(self.panel._queries), 0) @@ -396,3 +413,47 @@ def test_prettify_sql(self): self.panel.generate_stats(self.request, response) self.assertEqual(len(self.panel._queries), 1) self.assertEqual(pretty_sql, self.panel._queries[-1][1]["sql"]) + + @override_settings( + DEBUG=True, + ) + def test_flat_template_information(self): + """ + Test case for when the query is used in a flat template hierarchy + (without included templates). + """ + self.assertEqual(len(self.panel._queries), 0) + + users = User.objects.all() + render(self.request, "sql/flat.html", {"users": users}) + + self.assertEqual(len(self.panel._queries), 1) + + query = self.panel._queries[0] + template_info = query[1]["template_info"] + template_name = os.path.basename(template_info["name"]) + self.assertEqual(template_name, "flat.html") + self.assertEqual(template_info["context"][2]["content"].strip(), "{{ users }}") + self.assertEqual(template_info["context"][2]["highlight"], True) + + @override_settings( + DEBUG=True, + ) + def test_nested_template_information(self): + """ + Test case for when the query is used in a nested template + hierarchy (with included templates). + """ + self.assertEqual(len(self.panel._queries), 0) + + users = User.objects.all() + render(self.request, "sql/nested.html", {"users": users}) + + self.assertEqual(len(self.panel._queries), 1) + + query = self.panel._queries[0] + template_info = query[1]["template_info"] + template_name = os.path.basename(template_info["name"]) + self.assertEqual(template_name, "included.html") + self.assertEqual(template_info["context"][0]["content"].strip(), "{{ users }}") + self.assertEqual(template_info["context"][0]["highlight"], True) diff --git a/tests/templates/sql/flat.html b/tests/templates/sql/flat.html new file mode 100644 index 000000000..058dbe043 --- /dev/null +++ b/tests/templates/sql/flat.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block content %} + {{ users }} +{% endblock %} diff --git a/tests/templates/sql/included.html b/tests/templates/sql/included.html new file mode 100644 index 000000000..87d2e1f70 --- /dev/null +++ b/tests/templates/sql/included.html @@ -0,0 +1 @@ +{{ users }} diff --git a/tests/templates/sql/nested.html b/tests/templates/sql/nested.html new file mode 100644 index 000000000..8558e2d45 --- /dev/null +++ b/tests/templates/sql/nested.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block content %} + {% include "sql/included.html" %} +{% endblock %} diff --git a/tests/test_integration.py b/tests/test_integration.py index ebd4f882d..6d3208fff 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -121,6 +121,17 @@ def test_is_toolbar_request_without_djdt_urls(self): self.request.path = "/render_panel/" self.assertFalse(self.toolbar.is_toolbar_request(self.request)) + @override_settings(ROOT_URLCONF="tests.urls_invalid") + def test_is_toolbar_request_override_request_urlconf(self): + """Test cases when the toolbar URL is configured on the request.""" + self.request.path = "/__debug__/render_panel/" + self.assertFalse(self.toolbar.is_toolbar_request(self.request)) + + # Verify overriding the urlconf on the request is valid. + self.request.urlconf = "tests.urls" + self.request.path = "/__debug__/render_panel/" + self.assertTrue(self.toolbar.is_toolbar_request(self.request)) + @override_settings(DEBUG=True) class DebugToolbarIntegrationTestCase(IntegrationTestCase): @@ -321,11 +332,44 @@ def test_sql_profile_checks_show_toolbar(self): self.assertEqual(response.status_code, 404) @override_settings(DEBUG_TOOLBAR_CONFIG={"RENDER_PANELS": True}) - def test_data_store_id_not_rendered_when_none(self): + def test_render_panels_in_request(self): + """ + Test that panels are are rendered during the request with + RENDER_PANELS=TRUE + """ url = "/regular/basic/" response = self.client.get(url) self.assertIn(b'id="djDebug"', response.content) + # Verify the store id is not included. self.assertNotIn(b"data-store-id", response.content) + # Verify the history panel was disabled + self.assertIn( + b'', + response.content, + ) + # Verify the a panel was rendered + self.assertIn(b"Response headers", response.content) + + @override_settings(DEBUG_TOOLBAR_CONFIG={"RENDER_PANELS": False}) + def test_load_panels(self): + """ + Test that panels are not rendered during the request with + RENDER_PANELS=False + """ + url = "/execute_sql/" + response = self.client.get(url) + self.assertIn(b'id="djDebug"', response.content) + # Verify the store id is included. + self.assertIn(b"data-store-id", response.content) + # Verify the history panel was not disabled + self.assertNotIn( + b'', + response.content, + ) + # Verify the a panel was not rendered + self.assertNotIn(b"Response headers", response.content) def test_view_returns_template_response(self): response = self.client.get("/template_response/basic/") diff --git a/tox.ini b/tox.ini index 192e51f67..c3bb0bac2 100644 --- a/tox.ini +++ b/tox.ini @@ -2,15 +2,14 @@ envlist = docs style - readme - py{36,37}-dj{22,30,31,32}-sqlite - py{38,39}-dj{22,30,31,32,main}-sqlite - py{36,37,38,39}-dj{22,30,31,32}-{postgresql,mysql} + packaging + py{36,37}-dj{22,31,32}-sqlite + py{38,39}-dj{22,31,32,main}-sqlite + py{36,37,38,39}-dj{22,31,32}-{postgresql,mysql} [testenv] deps = dj22: Django==2.2.* - dj30: Django==3.0.* dj31: Django==3.1.* dj32: Django>=3.2a1,<4.0 sqlite: mock @@ -43,19 +42,19 @@ whitelist_externals = make pip_pre = True commands = make coverage TEST_ARGS='{posargs:tests}' -[testenv:py{36,37,38,39}-dj{22,30,31,32}-postgresql] +[testenv:py{36,37,38,39}-dj{22,31,32}-postgresql] setenv = {[testenv]setenv} DB_BACKEND = postgresql DB_PORT = {env:DB_PORT:5432} -[testenv:py{36,37,38,39}-dj{22,30,31,32}-mysql] +[testenv:py{36,37,38,39}-dj{22,31,32}-mysql] setenv = {[testenv]setenv} DB_BACKEND = mysql DB_PORT = {env:DB_PORT:3306} -[testenv:py{36,37,38,39}-dj{22,30,31,32,main}-sqlite] +[testenv:py{36,37,38,39}-dj{22,31,32,main}-sqlite] setenv = {[testenv]setenv} DB_BACKEND = sqlite3 @@ -75,9 +74,14 @@ deps = isort>=5.0.2 skip_install = true -[testenv:readme] -commands = python setup.py check -r -s -deps = readme_renderer +[testenv:packaging] +commands = + python setup.py sdist bdist_wheel + twine check --strict dist/* +deps = + readme_renderer + twine + wheel skip_install = true [gh-actions]