diff --git a/debug_toolbar/decorators.py b/debug_toolbar/decorators.py index 8114b05d7..e7dd58ea8 100644 --- a/debug_toolbar/decorators.py +++ b/debug_toolbar/decorators.py @@ -1,6 +1,9 @@ import functools from django.http import Http404 +from django.utils.translation import get_language, override as language_override + +from debug_toolbar import settings as dt_settings def require_show_toolbar(view): @@ -15,3 +18,16 @@ def inner(request, *args, **kwargs): return view(request, *args, **kwargs) return inner + + +def render_with_toolbar_language(view): + """Force any rendering within the view to use the toolbar's language.""" + + @functools.wraps(view) + def inner(request, *args, **kwargs): + + lang = dt_settings.get_config()["TOOLBAR_LANGUAGE"] or get_language() + with language_override(lang): + return view(request, *args, **kwargs) + + return inner diff --git a/debug_toolbar/panels/history/views.py b/debug_toolbar/panels/history/views.py index 7f8d0cb7c..3fcbd9b32 100644 --- a/debug_toolbar/panels/history/views.py +++ b/debug_toolbar/panels/history/views.py @@ -1,12 +1,13 @@ from django.http import HttpResponseBadRequest, JsonResponse from django.template.loader import render_to_string -from debug_toolbar.decorators import require_show_toolbar +from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar from debug_toolbar.panels.history.forms import HistoryStoreForm from debug_toolbar.toolbar import DebugToolbar @require_show_toolbar +@render_with_toolbar_language def history_sidebar(request): """Returns the selected debug toolbar history snapshot.""" form = HistoryStoreForm(request.GET) @@ -37,6 +38,7 @@ def history_sidebar(request): @require_show_toolbar +@render_with_toolbar_language def history_refresh(request): """Returns the refreshed list of table rows for the History Panel.""" form = HistoryStoreForm(request.GET) diff --git a/debug_toolbar/panels/sql/views.py b/debug_toolbar/panels/sql/views.py index fabca7a57..4b6ced9da 100644 --- a/debug_toolbar/panels/sql/views.py +++ b/debug_toolbar/panels/sql/views.py @@ -2,7 +2,7 @@ from django.template.loader import render_to_string from django.views.decorators.csrf import csrf_exempt -from debug_toolbar.decorators import require_show_toolbar +from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar from debug_toolbar.forms import SignedDataForm from debug_toolbar.panels.sql.forms import SQLSelectForm @@ -18,6 +18,7 @@ def get_signed_data(request): @csrf_exempt @require_show_toolbar +@render_with_toolbar_language def sql_select(request): """Returns the output of the SQL SELECT statement""" verified_data = get_signed_data(request) @@ -47,6 +48,7 @@ def sql_select(request): @csrf_exempt @require_show_toolbar +@render_with_toolbar_language def sql_explain(request): """Returns the output of the SQL EXPLAIN on the given query""" verified_data = get_signed_data(request) @@ -85,6 +87,7 @@ def sql_explain(request): @csrf_exempt @require_show_toolbar +@render_with_toolbar_language def sql_profile(request): """Returns the output of running the SQL and getting the profiling statistics""" verified_data = get_signed_data(request) diff --git a/debug_toolbar/panels/templates/views.py b/debug_toolbar/panels/templates/views.py index 134b3d476..e65d1a9d5 100644 --- a/debug_toolbar/panels/templates/views.py +++ b/debug_toolbar/panels/templates/views.py @@ -5,10 +5,11 @@ from django.template.loader import render_to_string from django.utils.html import format_html, mark_safe -from debug_toolbar.decorators import require_show_toolbar +from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar @require_show_toolbar +@render_with_toolbar_language def template_source(request): """ Return the source of a template, syntax-highlighted by Pygments if diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py index 1d319027d..b93acbeed 100644 --- a/debug_toolbar/views.py +++ b/debug_toolbar/views.py @@ -2,11 +2,12 @@ from django.utils.html import escape from django.utils.translation import gettext as _ -from debug_toolbar.decorators import require_show_toolbar +from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar from debug_toolbar.toolbar import DebugToolbar @require_show_toolbar +@render_with_toolbar_language def render_panel(request): """Render the contents of a panel""" toolbar = DebugToolbar.fetch(request.GET["store_id"]) diff --git a/docs/changes.rst b/docs/changes.rst index 0e4c1a28b..798ff43cf 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,9 @@ Pending ------- * Fixed PostgreSQL raw query with a tuple parameter during on explain. +* Use ``TOOLBAR_LANGUAGE`` setting when rendering individual panels + that are loaded via AJAX. +* Add decorator for rendering toolbar views with ``TOOLBAR_LANGUAGE``. 3.8.1 (2022-12-03) ------------------ @@ -27,6 +30,7 @@ Pending * Fix highlighting on history panel so odd rows are highlighted when selected. * Formalize support for Python 3.11. +* Added ``TOOLBAR_LANGUAGE`` setting. 3.7.0 (2022-09-25) ------------------ diff --git a/docs/configuration.rst b/docs/configuration.rst index 7949ae501..0aa7891a0 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -151,6 +151,8 @@ Toolbar options the request doesn't originate from the toolbar itself, EG that ``is_toolbar_request`` is false for a given request. +.. _TOOLBAR_LANGUAGE: + * ``TOOLBAR_LANGUAGE`` Default: ``None`` @@ -160,9 +162,7 @@ Toolbar options render the toolbar in a different language than what the application is rendered in. For example, if you wish to use English for development, but want to render your application in French, you would set this to - ``"en-us"`` and `settings.LANGUAGE_CODE`_ to ``"fr"``. - -.. _settings.LANGUAGE_CODE: https://docs.djangoproject.com/en/stable/ref/settings/#std-setting-LANGUAGE_CODE + ``"en-us"`` and :setting:`LANGUAGE_CODE` to ``"fr"``. Panel options ~~~~~~~~~~~~~ diff --git a/docs/panels.rst b/docs/panels.rst index 50090962d..795cb96a9 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -350,9 +350,18 @@ Third-party panels must subclass :class:`~debug_toolbar.panels.Panel`, according to the public API described below. Unless noted otherwise, all methods are optional. -Panels can ship their own templates, static files and views. All views should -be decorated with ``debug_toolbar.decorators.require_show_toolbar`` to prevent -unauthorized access. There is no public CSS API at this time. +Panels can ship their own templates, static files and views. + +Any views defined for the third-party panel use the following decorators: + +- ``debug_toolbar.decorators.require_show_toolbar`` - Prevents unauthorized + access to the view. +- ``debug_toolbar.decorators.render_with_toolbar_language`` - Supports + internationalization for any content rendered by the view. This will render + the response with the :ref:`TOOLBAR_LANGUAGE ` rather than + :setting:`LANGUAGE_CODE`. + +There is no public CSS API at this time. .. autoclass:: debug_toolbar.panels.Panel diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 000000000..5e7c8523b --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,26 @@ +from unittest.mock import patch + +from django.http import HttpResponse +from django.test import RequestFactory, TestCase +from django.test.utils import override_settings + +from debug_toolbar.decorators import render_with_toolbar_language + + +@render_with_toolbar_language +def stub_view(request): + return HttpResponse(200) + + +@override_settings(DEBUG=True, LANGUAGE_CODE="fr") +class RenderWithToolbarLanguageTestCase(TestCase): + @override_settings(DEBUG_TOOLBAR_CONFIG={"TOOLBAR_LANGUAGE": "de"}) + @patch("debug_toolbar.decorators.language_override") + def test_uses_toolbar_language(self, mock_language_override): + stub_view(RequestFactory().get("/")) + mock_language_override.assert_called_once_with("de") + + @patch("debug_toolbar.decorators.language_override") + def test_defaults_to_django_language_code(self, mock_language_override): + stub_view(RequestFactory().get("/")) + mock_language_override.assert_called_once_with("fr") diff --git a/tests/test_integration.py b/tests/test_integration.py index f41113938..b292dcbf0 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -671,8 +671,51 @@ def test_toolbar_language_will_render_to_default_language_when_not_set(self): hide_button = self.selenium.find_element(By.ID, "djHideToolBarButton") assert hide_button.text == "Hide »" + self.get("/execute_sql/") + sql_panel = self.selenium.find_element(By.ID, "SQLPanel") + + # Click to show the SQL panel + self.selenium.find_element(By.CLASS_NAME, "SQLPanel").click() + + table = self.wait.until( + lambda selenium: sql_panel.find_element(By.TAG_NAME, "table") + ) + self.assertIn("Query", table.text) + self.assertIn("Action", table.text) + @override_settings(DEBUG_TOOLBAR_CONFIG={"TOOLBAR_LANGUAGE": "pt-br"}) def test_toolbar_language_will_render_to_locale_when_set(self): self.get("/regular/basic/") hide_button = self.selenium.find_element(By.ID, "djHideToolBarButton") assert hide_button.text == "Esconder »" + + self.get("/execute_sql/") + sql_panel = self.selenium.find_element(By.ID, "SQLPanel") + + # Click to show the SQL panel + self.selenium.find_element(By.CLASS_NAME, "SQLPanel").click() + + table = self.wait.until( + lambda selenium: sql_panel.find_element(By.TAG_NAME, "table") + ) + self.assertIn("Query", table.text) + self.assertIn("Linha", table.text) + + @override_settings(DEBUG_TOOLBAR_CONFIG={"TOOLBAR_LANGUAGE": "en-us"}) + @override_settings(LANGUAGE_CODE="de") + def test_toolbar_language_will_render_to_locale_when_set_both(self): + self.get("/regular/basic/") + hide_button = self.selenium.find_element(By.ID, "djHideToolBarButton") + assert hide_button.text == "Hide »" + + self.get("/execute_sql/") + sql_panel = self.selenium.find_element(By.ID, "SQLPanel") + + # Click to show the SQL panel + self.selenium.find_element(By.CLASS_NAME, "SQLPanel").click() + + table = self.wait.until( + lambda selenium: sql_panel.find_element(By.TAG_NAME, "table") + ) + self.assertIn("Query", table.text) + self.assertIn("Action", table.text)