Browse Source

Fixed #32105 -- Added template paths as ExceptionReporter class attributes.

This allows replacement of the debugging templates without having to
copy-paste the `get_traceback_html` and `get_traceback_text` functions
into a subclass.

Thanks to Nick Pope for review.
Aarni Koskela 4 years ago
parent
commit
68e33b347d

+ 5 - 2
django/views/debug.py

@@ -245,6 +245,9 @@ class SafeExceptionReporterFilter:
 
 class ExceptionReporter:
     """Organize and coordinate reporting on exceptions."""
+    html_template_path = CURRENT_DIR / 'templates' / 'technical_500.html'
+    text_template_path = CURRENT_DIR / 'templates' / 'technical_500.txt'
+
     def __init__(self, request, exc_type, exc_value, tb, is_email=False):
         self.request = request
         self.filter = get_exception_reporter_filter(self.request)
@@ -331,14 +334,14 @@ class ExceptionReporter:
 
     def get_traceback_html(self):
         """Return HTML version of debug 500 HTTP error page."""
-        with Path(CURRENT_DIR, 'templates', 'technical_500.html').open(encoding='utf-8') as fh:
+        with self.html_template_path.open(encoding='utf-8') as fh:
             t = DEBUG_ENGINE.from_string(fh.read())
         c = Context(self.get_traceback_data(), use_l10n=False)
         return t.render(c)
 
     def get_traceback_text(self):
         """Return plain text version of debug 500 HTTP error page."""
-        with Path(CURRENT_DIR, 'templates', 'technical_500.txt').open(encoding='utf-8') as fh:
+        with self.text_template_path.open(encoding='utf-8') as fh:
             t = DEBUG_ENGINE.from_string(fh.read())
         c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False)
         return t.render(c)

+ 16 - 0
docs/howto/error-reporting.txt

@@ -325,6 +325,22 @@ Your custom reporter class needs to inherit from
 
 .. class:: ExceptionReporter
 
+    .. attribute:: html_template_path
+
+        .. versionadded:: 3.2
+
+        A :class:`pathlib.Path` representing the absolute filesystem path to a
+        template for rendering the HTML representation of the exception.
+        Defaults to the Django provided template.
+
+    .. attribute:: text_template_path
+
+        .. versionadded:: 3.2
+
+        A :class:`pathlib.Path` representing the absolute filesystem path to a
+        template for rendering the plain-text representation of the exception.
+        Defaults to the Django provided template.
+
     .. method:: get_traceback_data()
 
         Return a dictionary containing traceback information.

+ 4 - 1
docs/releases/3.2.txt

@@ -188,7 +188,10 @@ Email
 Error Reporting
 ~~~~~~~~~~~~~~~
 
-* ...
+* Custom :class:`~django.views.debug.ExceptionReporter` subclasses can now set
+  the :attr:`~django.views.debug.ExceptionReporter.html_template_path` and
+  :attr:`~django.views.debug.ExceptionReporter.text_template_path` class
+  attributes to override the templates used to render exception reports.
 
 File Storage
 ~~~~~~~~~~~~

+ 1 - 0
tests/view_tests/templates/my_technical_500.html

@@ -0,0 +1 @@
+<h1>Oh no, an error occurred!</h1>

+ 1 - 0
tests/view_tests/templates/my_technical_500.txt

@@ -0,0 +1 @@
+Oh dear, an error occurred!

+ 15 - 0
tests/view_tests/tests/test_debug.py

@@ -293,6 +293,21 @@ class DebugViewTests(SimpleTestCase):
             response = self.client.get('/raises500/')
         self.assertContains(response, 'custom traceback text', status_code=500)
 
+    @override_settings(DEFAULT_EXCEPTION_REPORTER='view_tests.views.TemplateOverrideExceptionReporter')
+    def test_template_override_exception_reporter(self):
+        with self.assertLogs('django.request', 'ERROR'):
+            response = self.client.get('/raises500/')
+        self.assertContains(
+            response,
+            '<h1>Oh no, an error occurred!</h1>',
+            status_code=500,
+            html=True,
+        )
+
+        with self.assertLogs('django.request', 'ERROR'):
+            response = self.client.get('/raises500/', HTTP_ACCEPT='text/plain')
+        self.assertContains(response, 'Oh dear, an error occurred!', status_code=500)
+
 
 class DebugViewQueriesAllowedTests(SimpleTestCase):
     # May need a query to initialize MySQL connection

+ 8 - 0
tests/view_tests/views.py

@@ -2,6 +2,7 @@ import datetime
 import decimal
 import logging
 import sys
+from pathlib import Path
 
 from django.core.exceptions import (
     BadRequest, PermissionDenied, SuspiciousOperation,
@@ -18,6 +19,8 @@ from django.views.decorators.debug import (
     sensitive_post_parameters, sensitive_variables,
 )
 
+TEMPLATES_PATH = Path(__file__).resolve().parent / 'templates'
+
 
 def index_page(request):
     """Dummy index page"""
@@ -240,6 +243,11 @@ class CustomExceptionReporter(ExceptionReporter):
         return self.custom_traceback_text
 
 
+class TemplateOverrideExceptionReporter(ExceptionReporter):
+    html_template_path = TEMPLATES_PATH / 'my_technical_500.html'
+    text_template_path = TEMPLATES_PATH / 'my_technical_500.txt'
+
+
 def custom_reporter_class_view(request):
     request.exception_reporter_class = CustomExceptionReporter
     try: