diff --git a/django/views/debug.py b/django/views/debug.py index bd0a6ba0ba..b555610d4a 100644 --- a/django/views/debug.py +++ b/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) diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index c77f1e955e..e2dbe7ca27 100644 --- a/docs/howto/error-reporting.txt +++ b/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. diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 0c233f796b..d61f66733d 100644 --- a/docs/releases/3.2.txt +++ b/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 ~~~~~~~~~~~~ diff --git a/tests/view_tests/templates/my_technical_500.html b/tests/view_tests/templates/my_technical_500.html new file mode 100644 index 0000000000..7e8f7a900d --- /dev/null +++ b/tests/view_tests/templates/my_technical_500.html @@ -0,0 +1 @@ +

Oh no, an error occurred!

diff --git a/tests/view_tests/templates/my_technical_500.txt b/tests/view_tests/templates/my_technical_500.txt new file mode 100644 index 0000000000..e2c7727981 --- /dev/null +++ b/tests/view_tests/templates/my_technical_500.txt @@ -0,0 +1 @@ +Oh dear, an error occurred! diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 0954a7c6bd..dcb0189b83 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/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, + '

Oh no, an error occurred!

', + 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 diff --git a/tests/view_tests/views.py b/tests/view_tests/views.py index 3d7d0908b0..ed84286173 100644 --- a/tests/view_tests/views.py +++ b/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: