From afeee8612dfede9aa7da06c3be625361f8861e45 Mon Sep 17 00:00:00 2001 From: Dan Jacob Date: Fri, 5 Jul 2024 14:10:17 +0300 Subject: [PATCH 1/7] Check for for StreamingHttpResponse when generating stats in Alert (#1946) * Check for for StreamingHttpResponse when generating stats in Alert panel. --- debug_toolbar/panels/alerts.py | 300 +++++++++++++++++---------------- docs/changes.rst | 5 + tests/panels/test_alerts.py | 213 ++++++++++++----------- 3 files changed, 269 insertions(+), 249 deletions(-) diff --git a/debug_toolbar/panels/alerts.py b/debug_toolbar/panels/alerts.py index 27a7119ee..32c656dde 100644 --- a/debug_toolbar/panels/alerts.py +++ b/debug_toolbar/panels/alerts.py @@ -1,148 +1,152 @@ -from html.parser import HTMLParser - -from django.utils.translation import gettext_lazy as _ - -from debug_toolbar.panels import Panel - - -class FormParser(HTMLParser): - """ - HTML form parser, used to check for invalid configurations of forms that - take file inputs. - """ - - def __init__(self): - super().__init__() - self.in_form = False - self.current_form = {} - self.forms = [] - self.form_ids = [] - self.referenced_file_inputs = [] - - def handle_starttag(self, tag, attrs): - attrs = dict(attrs) - if tag == "form": - self.in_form = True - form_id = attrs.get("id") - if form_id: - self.form_ids.append(form_id) - self.current_form = { - "file_form": False, - "form_attrs": attrs, - "submit_element_attrs": [], - } - elif ( - self.in_form - and tag == "input" - and attrs.get("type") == "file" - and (not attrs.get("form") or attrs.get("form") == "") - ): - self.current_form["file_form"] = True - elif ( - self.in_form - and ( - (tag == "input" and attrs.get("type") in {"submit", "image"}) - or tag == "button" - ) - and (not attrs.get("form") or attrs.get("form") == "") - ): - self.current_form["submit_element_attrs"].append(attrs) - elif tag == "input" and attrs.get("form"): - self.referenced_file_inputs.append(attrs) - - def handle_endtag(self, tag): - if tag == "form" and self.in_form: - self.forms.append(self.current_form) - self.in_form = False - - -class AlertsPanel(Panel): - """ - A panel to alert users to issues. - """ - - messages = { - "form_id_missing_enctype": _( - 'Form with id "{form_id}" contains file input, but does not have the attribute enctype="multipart/form-data".' - ), - "form_missing_enctype": _( - 'Form contains file input, but does not have the attribute enctype="multipart/form-data".' - ), - "input_refs_form_missing_enctype": _( - 'Input element references form with id "{form_id}", but the form does not have the attribute enctype="multipart/form-data".' - ), - } - - title = _("Alerts") - - template = "debug_toolbar/panels/alerts.html" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.alerts = [] - - @property - def nav_subtitle(self): - alerts = self.get_stats()["alerts"] - if alerts: - alert_text = "alert" if len(alerts) == 1 else "alerts" - return f"{len(alerts)} {alert_text}" - else: - return "" - - def add_alert(self, alert): - self.alerts.append(alert) - - def check_invalid_file_form_configuration(self, html_content): - """ - Inspects HTML content for a form that includes a file input but does - not have the encoding type set to multipart/form-data, and warns the - user if so. - """ - parser = FormParser() - parser.feed(html_content) - - # Check for file inputs directly inside a form that do not reference - # any form through the form attribute - for form in parser.forms: - if ( - form["file_form"] - and form["form_attrs"].get("enctype") != "multipart/form-data" - and not any( - elem.get("formenctype") == "multipart/form-data" - for elem in form["submit_element_attrs"] - ) - ): - if form_id := form["form_attrs"].get("id"): - alert = self.messages["form_id_missing_enctype"].format( - form_id=form_id - ) - else: - alert = self.messages["form_missing_enctype"] - self.add_alert({"alert": alert}) - - # Check for file inputs that reference a form - form_attrs_by_id = { - form["form_attrs"].get("id"): form["form_attrs"] for form in parser.forms - } - - for attrs in parser.referenced_file_inputs: - form_id = attrs.get("form") - if form_id and attrs.get("type") == "file": - form_attrs = form_attrs_by_id.get(form_id) - if form_attrs and form_attrs.get("enctype") != "multipart/form-data": - alert = self.messages["input_refs_form_missing_enctype"].format( - form_id=form_id - ) - self.add_alert({"alert": alert}) - - return self.alerts - - def generate_stats(self, request, response): - html_content = response.content.decode(response.charset) - self.check_invalid_file_form_configuration(html_content) - - # Further alert checks can go here - - # Write all alerts to record_stats - self.record_stats({"alerts": self.alerts}) +from html.parser import HTMLParser + +from django.utils.translation import gettext_lazy as _ + +from debug_toolbar.panels import Panel + + +class FormParser(HTMLParser): + """ + HTML form parser, used to check for invalid configurations of forms that + take file inputs. + """ + + def __init__(self): + super().__init__() + self.in_form = False + self.current_form = {} + self.forms = [] + self.form_ids = [] + self.referenced_file_inputs = [] + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if tag == "form": + self.in_form = True + form_id = attrs.get("id") + if form_id: + self.form_ids.append(form_id) + self.current_form = { + "file_form": False, + "form_attrs": attrs, + "submit_element_attrs": [], + } + elif ( + self.in_form + and tag == "input" + and attrs.get("type") == "file" + and (not attrs.get("form") or attrs.get("form") == "") + ): + self.current_form["file_form"] = True + elif ( + self.in_form + and ( + (tag == "input" and attrs.get("type") in {"submit", "image"}) + or tag == "button" + ) + and (not attrs.get("form") or attrs.get("form") == "") + ): + self.current_form["submit_element_attrs"].append(attrs) + elif tag == "input" and attrs.get("form"): + self.referenced_file_inputs.append(attrs) + + def handle_endtag(self, tag): + if tag == "form" and self.in_form: + self.forms.append(self.current_form) + self.in_form = False + + +class AlertsPanel(Panel): + """ + A panel to alert users to issues. + """ + + messages = { + "form_id_missing_enctype": _( + 'Form with id "{form_id}" contains file input, but does not have the attribute enctype="multipart/form-data".' + ), + "form_missing_enctype": _( + 'Form contains file input, but does not have the attribute enctype="multipart/form-data".' + ), + "input_refs_form_missing_enctype": _( + 'Input element references form with id "{form_id}", but the form does not have the attribute enctype="multipart/form-data".' + ), + } + + title = _("Alerts") + + template = "debug_toolbar/panels/alerts.html" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.alerts = [] + + @property + def nav_subtitle(self): + alerts = self.get_stats()["alerts"] + if alerts: + alert_text = "alert" if len(alerts) == 1 else "alerts" + return f"{len(alerts)} {alert_text}" + else: + return "" + + def add_alert(self, alert): + self.alerts.append(alert) + + def check_invalid_file_form_configuration(self, html_content): + """ + Inspects HTML content for a form that includes a file input but does + not have the encoding type set to multipart/form-data, and warns the + user if so. + """ + parser = FormParser() + parser.feed(html_content) + + # Check for file inputs directly inside a form that do not reference + # any form through the form attribute + for form in parser.forms: + if ( + form["file_form"] + and form["form_attrs"].get("enctype") != "multipart/form-data" + and not any( + elem.get("formenctype") == "multipart/form-data" + for elem in form["submit_element_attrs"] + ) + ): + if form_id := form["form_attrs"].get("id"): + alert = self.messages["form_id_missing_enctype"].format( + form_id=form_id + ) + else: + alert = self.messages["form_missing_enctype"] + self.add_alert({"alert": alert}) + + # Check for file inputs that reference a form + form_attrs_by_id = { + form["form_attrs"].get("id"): form["form_attrs"] for form in parser.forms + } + + for attrs in parser.referenced_file_inputs: + form_id = attrs.get("form") + if form_id and attrs.get("type") == "file": + form_attrs = form_attrs_by_id.get(form_id) + if form_attrs and form_attrs.get("enctype") != "multipart/form-data": + alert = self.messages["input_refs_form_missing_enctype"].format( + form_id=form_id + ) + self.add_alert({"alert": alert}) + + return self.alerts + + def generate_stats(self, request, response): + # check if streaming response + if getattr(response, "streaming", True): + return + + html_content = response.content.decode(response.charset) + self.check_invalid_file_form_configuration(html_content) + + # Further alert checks can go here + + # Write all alerts to record_stats + self.record_stats({"alerts": self.alerts}) diff --git a/docs/changes.rst b/docs/changes.rst index 4d26be57f..743623a24 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,11 @@ Change log Pending ------- +4.4.3 (2024-07-05) +------------------ + +* Added check for StreamingHttpResponse in Alert Panel + 4.4.3 (2024-07-04) ------------------ diff --git a/tests/panels/test_alerts.py b/tests/panels/test_alerts.py index e61c8da12..5c926f275 100644 --- a/tests/panels/test_alerts.py +++ b/tests/panels/test_alerts.py @@ -1,101 +1,112 @@ -from django.http import HttpResponse -from django.template import Context, Template - -from ..base import BaseTestCase - - -class AlertsPanelTestCase(BaseTestCase): - panel_id = "AlertsPanel" - - def test_alert_warning_display(self): - """ - Test that the panel (does not) display[s] an alert when there are - (no) problems. - """ - self.panel.record_stats({"alerts": []}) - self.assertNotIn("alerts", self.panel.nav_subtitle) - - self.panel.record_stats({"alerts": ["Alert 1", "Alert 2"]}) - self.assertIn("2 alerts", self.panel.nav_subtitle) - - def test_file_form_without_enctype_multipart_form_data(self): - """ - Test that the panel displays a form invalid message when there is - a file input but encoding not set to multipart/form-data. - """ - test_form = '
' - result = self.panel.check_invalid_file_form_configuration(test_form) - expected_error = ( - 'Form with id "test-form" contains file input, ' - 'but does not have the attribute enctype="multipart/form-data".' - ) - self.assertEqual(result[0]["alert"], expected_error) - self.assertEqual(len(result), 1) - - def test_file_form_no_id_without_enctype_multipart_form_data(self): - """ - Test that the panel displays a form invalid message when there is - a file input but encoding not set to multipart/form-data. - - This should use the message when the form has no id. - """ - test_form = '
' - result = self.panel.check_invalid_file_form_configuration(test_form) - expected_error = ( - "Form contains file input, but does not have " - 'the attribute enctype="multipart/form-data".' - ) - self.assertEqual(result[0]["alert"], expected_error) - self.assertEqual(len(result), 1) - - def test_file_form_with_enctype_multipart_form_data(self): - test_form = """
- -
""" - result = self.panel.check_invalid_file_form_configuration(test_form) - - self.assertEqual(len(result), 0) - - def test_file_form_with_enctype_multipart_form_data_in_button(self): - test_form = """
- - -
""" - result = self.panel.check_invalid_file_form_configuration(test_form) - - self.assertEqual(len(result), 0) - - def test_referenced_file_input_without_enctype_multipart_form_data(self): - test_file_input = """
- """ - result = self.panel.check_invalid_file_form_configuration(test_file_input) - - expected_error = ( - 'Input element references form with id "test-form", ' - 'but the form does not have the attribute enctype="multipart/form-data".' - ) - self.assertEqual(result[0]["alert"], expected_error) - self.assertEqual(len(result), 1) - - def test_referenced_file_input_with_enctype_multipart_form_data(self): - test_file_input = """
-
- """ - result = self.panel.check_invalid_file_form_configuration(test_file_input) - - self.assertEqual(len(result), 0) - - def test_integration_file_form_without_enctype_multipart_form_data(self): - t = Template('
') - c = Context({}) - rendered_template = t.render(c) - response = HttpResponse(content=rendered_template) - - self.panel.generate_stats(self.request, response) - - self.assertIn("1 alert", self.panel.nav_subtitle) - self.assertIn( - "Form with id "test-form" contains file input, " - "but does not have the attribute enctype="multipart/form-data".", - self.panel.content, - ) +from django.http import HttpResponse, StreamingHttpResponse +from django.template import Context, Template + +from ..base import BaseTestCase + + +class AlertsPanelTestCase(BaseTestCase): + panel_id = "AlertsPanel" + + def test_alert_warning_display(self): + """ + Test that the panel (does not) display[s] an alert when there are + (no) problems. + """ + self.panel.record_stats({"alerts": []}) + self.assertNotIn("alerts", self.panel.nav_subtitle) + + self.panel.record_stats({"alerts": ["Alert 1", "Alert 2"]}) + self.assertIn("2 alerts", self.panel.nav_subtitle) + + def test_file_form_without_enctype_multipart_form_data(self): + """ + Test that the panel displays a form invalid message when there is + a file input but encoding not set to multipart/form-data. + """ + test_form = '
' + result = self.panel.check_invalid_file_form_configuration(test_form) + expected_error = ( + 'Form with id "test-form" contains file input, ' + 'but does not have the attribute enctype="multipart/form-data".' + ) + self.assertEqual(result[0]["alert"], expected_error) + self.assertEqual(len(result), 1) + + def test_file_form_no_id_without_enctype_multipart_form_data(self): + """ + Test that the panel displays a form invalid message when there is + a file input but encoding not set to multipart/form-data. + + This should use the message when the form has no id. + """ + test_form = '
' + result = self.panel.check_invalid_file_form_configuration(test_form) + expected_error = ( + "Form contains file input, but does not have " + 'the attribute enctype="multipart/form-data".' + ) + self.assertEqual(result[0]["alert"], expected_error) + self.assertEqual(len(result), 1) + + def test_file_form_with_enctype_multipart_form_data(self): + test_form = """
+ +
""" + result = self.panel.check_invalid_file_form_configuration(test_form) + + self.assertEqual(len(result), 0) + + def test_file_form_with_enctype_multipart_form_data_in_button(self): + test_form = """
+ + +
""" + result = self.panel.check_invalid_file_form_configuration(test_form) + + self.assertEqual(len(result), 0) + + def test_referenced_file_input_without_enctype_multipart_form_data(self): + test_file_input = """
+ """ + result = self.panel.check_invalid_file_form_configuration(test_file_input) + + expected_error = ( + 'Input element references form with id "test-form", ' + 'but the form does not have the attribute enctype="multipart/form-data".' + ) + self.assertEqual(result[0]["alert"], expected_error) + self.assertEqual(len(result), 1) + + def test_referenced_file_input_with_enctype_multipart_form_data(self): + test_file_input = """
+
+ """ + result = self.panel.check_invalid_file_form_configuration(test_file_input) + + self.assertEqual(len(result), 0) + + def test_integration_file_form_without_enctype_multipart_form_data(self): + t = Template('
') + c = Context({}) + rendered_template = t.render(c) + response = HttpResponse(content=rendered_template) + + self.panel.generate_stats(self.request, response) + + self.assertIn("1 alert", self.panel.nav_subtitle) + self.assertIn( + "Form with id "test-form" contains file input, " + "but does not have the attribute enctype="multipart/form-data".", + self.panel.content, + ) + + def test_streaming_response(self): + """Test to check for a streaming response.""" + + def _render(): + yield "ok" + + response = StreamingHttpResponse(_render()) + + self.panel.generate_stats(self.request, response) + self.assertEqual(self.panel.get_stats(), {}) From e59c8ca522aabfa54c5ec5d06430ed9b147b8bec Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Fri, 5 Jul 2024 06:12:15 -0500 Subject: [PATCH 2/7] Clean-up change log. --- docs/changes.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 743623a24..fb64c78b5 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,15 +4,12 @@ Change log Pending ------- -4.4.3 (2024-07-05) ------------------- - -* Added check for StreamingHttpResponse in Alert Panel +* Added check for StreamingHttpResponse in alerts panel. 4.4.3 (2024-07-04) ------------------ -* Added alert panel with warning when form is using file fields +* Added alerts panel with warning when form is using file fields without proper encoding type. * Fixed overriding font-family for both light and dark themes. * Restored compatibility with ``iptools.IpRangeList``. From c8f676331893fe94abd3b2cc9623c4d2cc499960 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Mon, 12 Feb 2024 10:56:05 +0100 Subject: [PATCH 3/7] Instrument the Django Jinja2 template backend. - Add jinja2 template to example app. - Switch to the render function to include context. It instruments the single template render, but not the inherited templates and I'm guessing not the included templates either. I suspect we're going to have to patch jinja templates more robustly than relying on the django jinja backend template class. Co-Authored-By: Tim Schilling --- debug_toolbar/panels/templates/jinja2.py | 23 +++++++++++++++++++++++ debug_toolbar/panels/templates/panel.py | 2 ++ docs/changes.rst | 3 +++ example/settings.py | 8 +++++++- example/templates/index.html | 1 + example/templates/jinja2/index.jinja | 12 ++++++++++++ example/urls.py | 3 ++- example/views.py | 5 +++++ tests/panels/test_template.py | 5 ++++- tests/templates/jinja2/base.html | 9 +++++++++ tests/templates/jinja2/basic.jinja | 5 ++++- tests/views.py | 2 +- 12 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 debug_toolbar/panels/templates/jinja2.py create mode 100644 example/templates/jinja2/index.jinja create mode 100644 tests/templates/jinja2/base.html diff --git a/debug_toolbar/panels/templates/jinja2.py b/debug_toolbar/panels/templates/jinja2.py new file mode 100644 index 000000000..d343cb140 --- /dev/null +++ b/debug_toolbar/panels/templates/jinja2.py @@ -0,0 +1,23 @@ +import functools + +from django.template.backends.jinja2 import Template as JinjaTemplate +from django.template.context import make_context +from django.test.signals import template_rendered + + +def patch_jinja_render(): + orig_render = JinjaTemplate.render + + @functools.wraps(orig_render) + def wrapped_render(self, context=None, request=None): + # This patching of render only instruments the rendering + # of the immediate template. It won't include the parent template(s). + self.name = self.template.name + template_rendered.send( + sender=self, template=self, context=make_context(context, request) + ) + return orig_render(self, context, request) + + if JinjaTemplate.render != wrapped_render: + JinjaTemplate.original_render = JinjaTemplate.render + JinjaTemplate.render = wrapped_render diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index 81d7e5fad..182f80aab 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -14,6 +14,7 @@ from debug_toolbar.panels import Panel from debug_toolbar.panels.sql.tracking import SQLQueryTriggered, allow_sql from debug_toolbar.panels.templates import views +from debug_toolbar.panels.templates.jinja2 import patch_jinja_render # Monkey-patch to enable the template_rendered signal. The receiver returns # immediately when the panel is disabled to keep the overhead small. @@ -25,6 +26,7 @@ Template.original_render = Template._render Template._render = instrumented_test_render +patch_jinja_render() # Monkey-patch to store items added by template context processors. The # overhead is sufficiently small to justify enabling it unconditionally. diff --git a/docs/changes.rst b/docs/changes.rst index fb64c78b5..2ab16b544 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,9 @@ Pending ------- * Added check for StreamingHttpResponse in alerts panel. +* Instrument the Django Jinja2 template backend. This only instruments + the immediate template that's rendered. It will not provide stats on + any parent templates. 4.4.3 (2024-07-04) ------------------ diff --git a/example/settings.py b/example/settings.py index 1508b5a29..26b75fa5c 100644 --- a/example/settings.py +++ b/example/settings.py @@ -41,6 +41,12 @@ STATIC_URL = "/static/" TEMPLATES = [ + { + "NAME": "jinja2", + "BACKEND": "django.template.backends.jinja2.Jinja2", + "APP_DIRS": True, + "DIRS": [os.path.join(BASE_DIR, "example", "templates", "jinja2")], + }, { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, @@ -54,7 +60,7 @@ "django.contrib.messages.context_processors.messages", ], }, - } + }, ] USE_TZ = True diff --git a/example/templates/index.html b/example/templates/index.html index 527f5d2a3..4b25aefca 100644 --- a/example/templates/index.html +++ b/example/templates/index.html @@ -9,6 +9,7 @@

Index of Tests

{% cache 10 index_cache %}
    +
  • Jinja2
  • jQuery 3.3.1
  • MooTools 1.6.0
  • Prototype 1.7.3.0
  • diff --git a/example/templates/jinja2/index.jinja b/example/templates/jinja2/index.jinja new file mode 100644 index 000000000..ffd1ada6f --- /dev/null +++ b/example/templates/jinja2/index.jinja @@ -0,0 +1,12 @@ + + + + + jinja Test + + +

    jinja Test

    + {{ foo }} + {% for i in range(10) %}{{ i }}{% endfor %} {# Jinja2 supports range(), Django templates do not #} + + diff --git a/example/urls.py b/example/urls.py index 6dded2da7..c5e60c309 100644 --- a/example/urls.py +++ b/example/urls.py @@ -3,7 +3,7 @@ from django.views.generic import TemplateView from debug_toolbar.toolbar import debug_toolbar_urls -from example.views import increment +from example.views import increment, jinja2_view urlpatterns = [ path("", TemplateView.as_view(template_name="index.html"), name="home"), @@ -12,6 +12,7 @@ TemplateView.as_view(template_name="bad_form.html"), name="bad_form", ), + path("jinja/", jinja2_view, name="jinja"), path("jquery/", TemplateView.as_view(template_name="jquery/index.html")), path("mootools/", TemplateView.as_view(template_name="mootools/index.html")), path("prototype/", TemplateView.as_view(template_name="prototype/index.html")), diff --git a/example/views.py b/example/views.py index 46136515e..e7e4c1253 100644 --- a/example/views.py +++ b/example/views.py @@ -1,4 +1,5 @@ from django.http import JsonResponse +from django.shortcuts import render def increment(request): @@ -8,3 +9,7 @@ def increment(request): value = 1 request.session["value"] = value return JsonResponse({"value": value}) + + +def jinja2_view(request): + return render(request, "index.jinja", {"foo": "bar"}, using="jinja2") diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index eb23cde31..2bd02bf1d 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -1,3 +1,5 @@ +from unittest import expectedFailure + import django from django.contrib.auth.models import User from django.template import Context, RequestContext, Template @@ -135,11 +137,12 @@ def test_lazyobject_eval(self): DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"] ) class JinjaTemplateTestCase(IntegrationTestCase): + @expectedFailure def test_django_jinja2(self): r = self.client.get("/regular_jinja/foobar/") self.assertContains(r, "Test for foobar (Jinja)") self.assertContains(r, "

    Templates (2 rendered)

    ") - self.assertContains(r, "jinja2/basic.jinja") + self.assertContains(r, "basic.jinja") def context_processor(request): diff --git a/tests/templates/jinja2/base.html b/tests/templates/jinja2/base.html new file mode 100644 index 000000000..ea0d773ac --- /dev/null +++ b/tests/templates/jinja2/base.html @@ -0,0 +1,9 @@ + + + + {{ title }} + + + {% block content %}{% endblock %} + + diff --git a/tests/templates/jinja2/basic.jinja b/tests/templates/jinja2/basic.jinja index 812acbcac..e531eee64 100644 --- a/tests/templates/jinja2/basic.jinja +++ b/tests/templates/jinja2/basic.jinja @@ -1,2 +1,5 @@ {% extends 'base.html' %} -{% block content %}Test for {{ title }} (Jinja){% endblock %} +{% block content %} +Test for {{ title }} (Jinja) +{% for i in range(10) %}{{ i }}{% endfor %} {# Jinja2 supports range(), Django templates do not #} +{% endblock %} diff --git a/tests/views.py b/tests/views.py index c7214029e..8ae4631fe 100644 --- a/tests/views.py +++ b/tests/views.py @@ -48,7 +48,7 @@ def json_view(request): def regular_jinjia_view(request, title): - return render(request, "jinja2/basic.jinja", {"title": title}) + return render(request, "basic.jinja", {"title": title}, using="jinja2") def listcomp_view(request): From 4fd886bf91123e224f8f39e7abf4ff48a8ae5a35 Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Thu, 4 Jul 2024 20:42:18 -0500 Subject: [PATCH 4/7] Improve the jinja tests to better indicate the situation. --- tests/panels/test_template.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index 2bd02bf1d..636e88a23 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -137,8 +137,20 @@ def test_lazyobject_eval(self): DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"] ) class JinjaTemplateTestCase(IntegrationTestCase): - @expectedFailure def test_django_jinja2(self): + r = self.client.get("/regular_jinja/foobar/") + self.assertContains(r, "Test for foobar (Jinja)") + # This should be 2 templates because of the parent template. + # See test_django_jinja2_parent_template_instrumented + self.assertContains(r, "

    Templates (1 rendered)

    ") + self.assertContains(r, "basic.jinja") + + @expectedFailure + def test_django_jinja2_parent_template_instrumented(self): + """ + When Jinja2 templates are properly instrumented, the + parent template should be instrumented. + """ r = self.client.get("/regular_jinja/foobar/") self.assertContains(r, "Test for foobar (Jinja)") self.assertContains(r, "

    Templates (2 rendered)

    ") From 661491059df0e0f2321bf71bafe58c2dccb672b6 Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Fri, 5 Jul 2024 06:28:31 -0500 Subject: [PATCH 5/7] Fix jinja2 integration test. Now that we're using the actual jinja template backend, it only instruments a single template. --- tests/test_integration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 71525affe..4899e7c0f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -622,8 +622,9 @@ def test_basic_jinja(self): # Click to show the template panel self.selenium.find_element(By.CLASS_NAME, "TemplatesPanel").click() - - self.assertIn("Templates (2 rendered)", template_panel.text) + # This should be 2 templates rendered. See + # JinjaTemplateTestCase.test_django_jinja2_parent_template_instrumented + self.assertIn("Templates (1 rendered)", template_panel.text) self.assertIn("base.html", template_panel.text) self.assertIn("jinja2/basic.jinja", template_panel.text) From 9834e7eed992055ff1408efb849ba38817ca3b5f Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Fri, 5 Jul 2024 06:40:11 -0500 Subject: [PATCH 6/7] Ignore check for jinja2's base.html template in integration test --- tests/test_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 4899e7c0f..95207c21b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -622,10 +622,10 @@ def test_basic_jinja(self): # Click to show the template panel self.selenium.find_element(By.CLASS_NAME, "TemplatesPanel").click() - # This should be 2 templates rendered. See + # This should be 2 templates rendered, including base.html See # JinjaTemplateTestCase.test_django_jinja2_parent_template_instrumented self.assertIn("Templates (1 rendered)", template_panel.text) - self.assertIn("base.html", template_panel.text) + self.assertNotIn("base.html", template_panel.text) self.assertIn("jinja2/basic.jinja", template_panel.text) @override_settings( From 57ada8e90c16d8973ca84c9e81b700ff0cb0d53c Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Fri, 5 Jul 2024 06:16:51 -0500 Subject: [PATCH 7/7] Version 4.4.4 --- README.rst | 2 +- debug_toolbar/__init__.py | 2 +- docs/changes.rst | 3 +++ docs/conf.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 2ce1db4b7..fa12e35c1 100644 --- a/README.rst +++ b/README.rst @@ -44,7 +44,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 4.4.3. It works on +The current stable version of the Debug Toolbar is 4.4.4. It works on Django ≥ 4.2.0. The Debug Toolbar does not currently support `Django's asynchronous views diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py index 5ddb15d15..f5f18057b 100644 --- a/debug_toolbar/__init__.py +++ b/debug_toolbar/__init__.py @@ -4,7 +4,7 @@ # Do not use pkg_resources to find the version but set it here directly! # see issue #1446 -VERSION = "4.4.3" +VERSION = "4.4.4" # Code that discovers files or modules in INSTALLED_APPS imports this module. urls = "debug_toolbar.urls", APP_NAME diff --git a/docs/changes.rst b/docs/changes.rst index 2ab16b544..539c9883c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,9 @@ Change log Pending ------- +4.4.4 (2024-07-05) +------------------ + * Added check for StreamingHttpResponse in alerts panel. * Instrument the Django Jinja2 template backend. This only instruments the immediate template that's rendered. It will not provide stats on diff --git a/docs/conf.py b/docs/conf.py index 5f69100f7..b155e44ef 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 = "4.4.3" +release = "4.4.4" # -- General configuration ---------------------------------------------------