Browse Source

Fixed #35143 -- Improved accessibility of 404/500 debug pages.

This:
- changes the header, main, and footer content areas to be rendered
  in a <header>, <main>, and <footer> tags,
- adds scope attributes to <th>,
- uses <code> for a patterns list,
- uses <small> instead of <span>.
Marijke Luttekes 1 year ago
parent
commit
b9e2a3fc63

+ 1 - 0
AUTHORS

@@ -638,6 +638,7 @@ answer newbie questions, and generally made Django that much better:
     Marc Tamlyn <marc.tamlyn@gmail.com>
     Marc-Aurèle Brothier <ma.brothier@gmail.com>
     Marian Andre <django@andre.sk>
+    Marijke Luttekes <mail@marijkeluttekes.dev>
     Marijn Vriens <marijn@metronomo.cl>
     Mario Gonzalez <gonzalemario@gmail.com>
     Mariusz Felisiak <felisiak.mariusz@gmail.com>

+ 15 - 12
django/views/templates/technical_404.html

@@ -9,9 +9,9 @@
     body * { padding:10px 20px; }
     body * * { padding:0; }
     body { font:small sans-serif; background:#eee; color:#000; }
-    body>div { border-bottom:1px solid #ddd; }
+    body > :where(header, main, footer) { border-bottom:1px solid #ddd; }
     h1 { font-weight:normal; margin-bottom:.4em; }
-    h1 span { font-size:60%; color:#666; font-weight:normal; }
+    h1 small { font-size:60%; color:#666; font-weight:normal; }
     table { border:none; border-collapse: collapse; width:100%; }
     td, th { vertical-align:top; padding:2px 3px; }
     th { width:12em; text-align:right; color:#666; padding-right:.5em; }
@@ -24,27 +24,28 @@
   </style>
 </head>
 <body>
-  <div id="summary">
-    <h1>Page not found <span>(404)</span></h1>
+  <header id="summary">
+    <h1>Page not found <small>(404)</small></h1>
     {% if reason and resolved %}<pre class="exception_value">{{ reason }}</pre>{% endif %}
     <table class="meta">
       <tr>
-        <th>Request Method:</th>
+        <th scope="row">Request Method:</th>
         <td>{{ request.META.REQUEST_METHOD }}</td>
       </tr>
       <tr>
-        <th>Request URL:</th>
+        <th scope="row">Request URL:</th>
         <td>{{ request.build_absolute_uri }}</td>
       </tr>
       {% if raising_view_name %}
       <tr>
-        <th>Raised by:</th>
+        <th scope="row">Raised by:</th>
         <td>{{ raising_view_name }}</td>
       </tr>
       {% endif %}
     </table>
-  </div>
-  <div id="info">
+  </header>
+
+  <main id="info">
     {% if urlpatterns %}
       <p>
       Using the URLconf defined in <code>{{ urlconf }}</code>,
@@ -54,8 +55,10 @@
         {% for pattern in urlpatterns %}
           <li>
             {% for pat in pattern %}
+              <code>
                 {{ pat.pattern }}
                 {% if forloop.last and pat.name %}[name='{{ pat.name }}']{% endif %}
+              </code>
             {% endfor %}
           </li>
         {% endfor %}
@@ -69,14 +72,14 @@
         {% if resolved %}matched the last one.{% else %}didn’t match any of these.{% endif %}
       </p>
     {% endif %}
-  </div>
+  </main>
 
-  <div id="explanation">
+  <footer id="explanation">
     <p>
       You’re seeing this error because you have <code>DEBUG = True</code> in
       your Django settings file. Change that to <code>False</code>, and Django
       will display a standard 404 page.
     </p>
-  </div>
+  </footer>
 </body>
 </html>

+ 39 - 33
django/views/templates/technical_500.html

@@ -10,7 +10,7 @@
     body * { padding:10px 20px; }
     body * * { padding:0; }
     body { font:small sans-serif; background-color:#fff; color:#000; }
-    body>div { border-bottom:1px solid #ddd; }
+    body > :where(header, main, footer) { border-bottom:1px solid #ddd; }
     h1 { font-weight:normal; }
     h2 { margin-bottom:.8em; }
     h3 { margin:1em 0 .5em 0; }
@@ -47,6 +47,8 @@
     .user div.commands a { color: black; }
     #summary { background: #ffc; }
     #summary h2 { font-weight: normal; color: #666; }
+    #info { padding: 0; }
+    #info > * { padding:10px 20px; }
     #explanation { background:#eee; }
     #template, #template-not-exist { background:#f6f6f6; }
     #template-not-exist ul { margin: 0 0 10px 20px; }
@@ -97,67 +99,69 @@
   {% endif %}
 </head>
 <body>
-<div id="summary">
+<header id="summary">
   <h1>{% if exception_type %}{{ exception_type }}{% else %}Report{% endif %}
       {% if request %} at {{ request.path_info }}{% endif %}</h1>
   <pre class="exception_value">{% if exception_value %}{{ exception_value|force_escape }}{% if exception_notes %}{{ exception_notes }}{% endif %}{% else %}No exception message supplied{% endif %}</pre>
   <table class="meta">
 {% if request %}
     <tr>
-      <th>Request Method:</th>
+      <th scope="row">Request Method:</th>
       <td>{{ request.META.REQUEST_METHOD }}</td>
     </tr>
     <tr>
-      <th>Request URL:</th>
+      <th scope="row">Request URL:</th>
       <td>{{ request_insecure_uri }}</td>
     </tr>
 {% endif %}
     <tr>
-      <th>Django Version:</th>
+      <th scope="row">Django Version:</th>
       <td>{{ django_version_info }}</td>
     </tr>
 {% if exception_type %}
     <tr>
-      <th>Exception Type:</th>
+      <th scope="row">Exception Type:</th>
       <td>{{ exception_type }}</td>
     </tr>
 {% endif %}
 {% if exception_type and exception_value %}
     <tr>
-      <th>Exception Value:</th>
+      <th scope="row">Exception Value:</th>
       <td><pre>{{ exception_value|force_escape }}</pre></td>
     </tr>
 {% endif %}
 {% if lastframe %}
     <tr>
-      <th>Exception Location:</th>
+      <th scope="row">Exception Location:</th>
       <td><span class="fname">{{ lastframe.filename }}</span>, line {{ lastframe.lineno }}, in {{ lastframe.function }}</td>
     </tr>
 {% endif %}
 {% if raising_view_name %}
     <tr>
-      <th>Raised during:</th>
+      <th scope="row">Raised during:</th>
       <td>{{ raising_view_name }}</td>
     </tr>
 {% endif %}
     <tr>
-      <th>Python Executable:</th>
+      <th scope="row">Python Executable:</th>
       <td>{{ sys_executable }}</td>
     </tr>
     <tr>
-      <th>Python Version:</th>
+      <th scope="row">Python Version:</th>
       <td>{{ sys_version_info }}</td>
     </tr>
     <tr>
-      <th>Python Path:</th>
-      <td><pre>{{ sys_path|pprint }}</pre></td>
+      <th scope="row">Python Path:</th>
+      <td><pre><code>{{ sys_path|pprint }}</code></pre></td>
     </tr>
     <tr>
-      <th>Server time:</th>
+      <th scope="row">Server time:</th>
       <td>{{server_time|date:"r"}}</td>
     </tr>
   </table>
-</div>
+</header>
+
+<main id="info">
 {% if unicode_hint %}
 <div id="unicode-hint">
     <h2>Unicode error hint</h2>
@@ -195,11 +199,11 @@
       {% if template_info.bottom != template_info.total %} cut-bottom{% endif %}">
    {% for source_line in template_info.source_lines %}
    {% if source_line.0 == template_info.line %}
-   <tr class="error"><th>{{ source_line.0 }}</th>
+   <tr class="error"><th scope="row">{{ source_line.0 }}</th>
      <td>{{ template_info.before }}<span class="specific">{{ template_info.during }}</span>{{ template_info.after }}</td>
    </tr>
    {% else %}
-      <tr><th>{{ source_line.0 }}</th>
+      <tr><th scope="row">{{ source_line.0 }}</th>
       <td>{{ source_line.1 }}</td></tr>
    {% endif %}
    {% endfor %}
@@ -266,8 +270,8 @@
             <table class="vars" id="v{{ frame.id }}">
               <thead>
                 <tr>
-                  <th>Variable</th>
-                  <th>Value</th>
+                  <th scope="col">Variable</th>
+                  <th scope="col">Value</th>
                 </tr>
               </thead>
               <tbody>
@@ -354,8 +358,8 @@ Exception Value: {{ exception_value|force_escape }}{% if exception_notes %}{{ ex
     <table class="req">
       <thead>
         <tr>
-          <th>Variable</th>
-          <th>Value</th>
+          <th scope="col">Variable</th>
+          <th scope="col">Value</th>
         </tr>
       </thead>
       <tbody>
@@ -376,8 +380,8 @@ Exception Value: {{ exception_value|force_escape }}{% if exception_notes %}{{ ex
     <table class="req">
       <thead>
         <tr>
-          <th>Variable</th>
-          <th>Value</th>
+          <th scope="col">Variable</th>
+          <th scope="col">Value</th>
         </tr>
       </thead>
       <tbody>
@@ -398,8 +402,8 @@ Exception Value: {{ exception_value|force_escape }}{% if exception_notes %}{{ ex
     <table class="req">
       <thead>
         <tr>
-          <th>Variable</th>
-          <th>Value</th>
+          <th scope="col">Variable</th>
+          <th scope="col">Value</th>
         </tr>
       </thead>
       <tbody>
@@ -420,8 +424,8 @@ Exception Value: {{ exception_value|force_escape }}{% if exception_notes %}{{ ex
     <table class="req">
       <thead>
         <tr>
-          <th>Variable</th>
-          <th>Value</th>
+          <th scope="col">Variable</th>
+          <th scope="col">Value</th>
         </tr>
       </thead>
       <tbody>
@@ -441,8 +445,8 @@ Exception Value: {{ exception_value|force_escape }}{% if exception_notes %}{{ ex
   <table class="req">
     <thead>
       <tr>
-        <th>Variable</th>
-        <th>Value</th>
+        <th scope="col">Variable</th>
+        <th scope="col">Value</th>
       </tr>
     </thead>
     <tbody>
@@ -463,8 +467,8 @@ Exception Value: {{ exception_value|force_escape }}{% if exception_notes %}{{ ex
   <table class="req">
     <thead>
       <tr>
-        <th>Setting</th>
-        <th>Value</th>
+        <th scope="col">Setting</th>
+        <th scope="col">Value</th>
       </tr>
     </thead>
     <tbody>
@@ -478,14 +482,16 @@ Exception Value: {{ exception_value|force_escape }}{% if exception_notes %}{{ ex
   </table>
 
 </div>
+</main>
+
 {% if not is_email %}
-  <div id="explanation">
+  <footer id="explanation">
     <p>
       You’re seeing this error because you have <code>DEBUG = True</code> in your
       Django settings file. Change that to <code>False</code>, and Django will
       display a standard page generated by the handler for this status code.
     </p>
-  </div>
+  </footer>
 {% endif %}
 </body>
 </html>

+ 2 - 1
docs/releases/5.1.txt

@@ -156,7 +156,8 @@ Email
 Error Reporting
 ~~~~~~~~~~~~~~~
 
-* ...
+* In order to improve accessibility, the technical 404 and 500 error pages now
+  use HTML landmark elements for the header, footer, and main content areas.
 
 File Storage
 ~~~~~~~~~~~~

+ 43 - 30
tests/view_tests/tests/test_debug.py

@@ -176,6 +176,12 @@ class DebugViewTests(SimpleTestCase):
         self.assertContains(
             response, "Django tried these URL patterns", status_code=404
         )
+        self.assertContains(
+            response,
+            "<code>technical404/ [name='my404']</code>",
+            status_code=404,
+            html=True,
+        )
         self.assertContains(
             response,
             "<p>The current path, <code>not-in-urls</code>, didn’t match any "
@@ -204,6 +210,9 @@ class DebugViewTests(SimpleTestCase):
 
     def test_technical_404(self):
         response = self.client.get("/technical404/")
+        self.assertContains(response, '<header id="summary">', status_code=404)
+        self.assertContains(response, '<main id="info">', status_code=404)
+        self.assertContains(response, '<footer id="explanation">', status_code=404)
         self.assertContains(
             response,
             '<pre class="exception_value">Testing technical 404.</pre>',
@@ -228,7 +237,7 @@ class DebugViewTests(SimpleTestCase):
         response = self.client.get("/classbased404/")
         self.assertContains(
             response,
-            "<th>Raised by:</th><td>view_tests.views.Http404View</td>",
+            '<th scope="row">Raised by:</th><td>view_tests.views.Http404View</td>',
             status_code=404,
             html=True,
         )
@@ -236,9 +245,12 @@ class DebugViewTests(SimpleTestCase):
     def test_technical_500(self):
         with self.assertLogs("django.request", "ERROR"):
             response = self.client.get("/raises500/")
+        self.assertContains(response, '<header id="summary">', status_code=500)
+        self.assertContains(response, '<main id="info">', status_code=500)
+        self.assertContains(response, '<footer id="explanation">', status_code=500)
         self.assertContains(
             response,
-            "<th>Raised during:</th><td>view_tests.views.raises500</td>",
+            '<th scope="row">Raised during:</th><td>view_tests.views.raises500</td>',
             status_code=500,
             html=True,
         )
@@ -255,7 +267,8 @@ class DebugViewTests(SimpleTestCase):
             response = self.client.get("/classbased500/")
         self.assertContains(
             response,
-            "<th>Raised during:</th><td>view_tests.views.Raises500View</td>",
+            '<th scope="row">Raised during:</th>'
+            "<td>view_tests.views.Raises500View</td>",
             status_code=500,
             html=True,
         )
@@ -397,7 +410,7 @@ class DebugViewTests(SimpleTestCase):
         """
         response = self.client.get("/")
         self.assertContains(
-            response, "Page not found <span>(404)</span>", status_code=404
+            response, "Page not found <small>(404)</small>", status_code=404
         )
 
     def test_template_encoding(self):
@@ -531,12 +544,12 @@ class ExceptionReporterTests(SimpleTestCase):
         self.assertIn(
             '<pre class="exception_value">Can&#x27;t find my keys</pre>', html
         )
-        self.assertIn("<th>Request Method:</th>", html)
-        self.assertIn("<th>Request URL:</th>", html)
+        self.assertIn('<th scope="row">Request Method:</th>', html)
+        self.assertIn('<th scope="row">Request URL:</th>', html)
         self.assertIn('<h3 id="user-info">USER</h3>', html)
         self.assertIn("<p>jacob</p>", html)
-        self.assertIn("<th>Exception Type:</th>", html)
-        self.assertIn("<th>Exception Value:</th>", html)
+        self.assertIn('<th scope="row">Exception Type:</th>', html)
+        self.assertIn('<th scope="row">Exception Value:</th>', html)
         self.assertIn("<h2>Traceback ", html)
         self.assertIn("<h2>Request information</h2>", html)
         self.assertNotIn("<p>Request data not supplied</p>", html)
@@ -554,11 +567,11 @@ class ExceptionReporterTests(SimpleTestCase):
         self.assertIn(
             '<pre class="exception_value">Can&#x27;t find my keys</pre>', html
         )
-        self.assertNotIn("<th>Request Method:</th>", html)
-        self.assertNotIn("<th>Request URL:</th>", html)
+        self.assertNotIn('<th scope="row">Request Method:</th>', html)
+        self.assertNotIn('<th scope="row">Request URL:</th>', html)
         self.assertNotIn('<h3 id="user-info">USER</h3>', html)
-        self.assertIn("<th>Exception Type:</th>", html)
-        self.assertIn("<th>Exception Value:</th>", html)
+        self.assertIn('<th scope="row">Exception Type:</th>', html)
+        self.assertIn('<th scope="row">Exception Value:</th>', html)
         self.assertIn("<h2>Traceback ", html)
         self.assertIn("<h2>Request information</h2>", html)
         self.assertIn("<p>Request data not supplied</p>", html)
@@ -603,10 +616,10 @@ class ExceptionReporterTests(SimpleTestCase):
         self.assertIn(
             '<pre class="exception_value">No exception message supplied</pre>', html
         )
-        self.assertIn("<th>Request Method:</th>", html)
-        self.assertIn("<th>Request URL:</th>", html)
-        self.assertNotIn("<th>Exception Type:</th>", html)
-        self.assertNotIn("<th>Exception Value:</th>", html)
+        self.assertIn('<th scope="row">Request Method:</th>', html)
+        self.assertIn('<th scope="row">Request URL:</th>', html)
+        self.assertNotIn('<th scope="row">Exception Type:</th>', html)
+        self.assertNotIn('<th scope="row">Exception Value:</th>', html)
         self.assertNotIn("<h2>Traceback ", html)
         self.assertIn("<h2>Request information</h2>", html)
         self.assertNotIn("<p>Request data not supplied</p>", html)
@@ -626,8 +639,8 @@ class ExceptionReporterTests(SimpleTestCase):
         self.assertIn(
             '<pre class="exception_value">Can&#x27;t find my keys</pre>', html
         )
-        self.assertIn("<th>Exception Type:</th>", html)
-        self.assertIn("<th>Exception Value:</th>", html)
+        self.assertIn('<th scope="row">Exception Type:</th>', html)
+        self.assertIn('<th scope="row">Exception Value:</th>', html)
         self.assertIn("<h2>Traceback ", html)
         self.assertIn("<h2>Request information</h2>", html)
         self.assertIn("<p>Request data not supplied</p>", html)
@@ -650,8 +663,8 @@ class ExceptionReporterTests(SimpleTestCase):
         html = reporter.get_traceback_html()
         self.assertInHTML("<h1>RuntimeError</h1>", html)
         self.assertIn('<pre class="exception_value">Oops</pre>', html)
-        self.assertIn("<th>Exception Type:</th>", html)
-        self.assertIn("<th>Exception Value:</th>", html)
+        self.assertIn('<th scope="row">Exception Type:</th>', html)
+        self.assertIn('<th scope="row">Exception Value:</th>', html)
         self.assertIn("<h2>Traceback ", html)
         self.assertIn("<h2>Request information</h2>", html)
         self.assertIn("<p>Request data not supplied</p>", html)
@@ -721,8 +734,8 @@ class ExceptionReporterTests(SimpleTestCase):
         html = reporter.get_traceback_html()
         self.assertInHTML("<h1>RuntimeError</h1>", html)
         self.assertIn('<pre class="exception_value">Oops</pre>', html)
-        self.assertIn("<th>Exception Type:</th>", html)
-        self.assertIn("<th>Exception Value:</th>", html)
+        self.assertIn('<th scope="row">Exception Type:</th>', html)
+        self.assertIn('<th scope="row">Exception Value:</th>', html)
         self.assertIn("<h2>Traceback ", html)
         self.assertInHTML('<li class="frame user">Traceback: None</li>', html)
         self.assertIn(
@@ -981,10 +994,10 @@ class ExceptionReporterTests(SimpleTestCase):
         self.assertIn(
             '<pre class="exception_value">I&#x27;m a little teapot</pre>', html
         )
-        self.assertIn("<th>Request Method:</th>", html)
-        self.assertIn("<th>Request URL:</th>", html)
-        self.assertNotIn("<th>Exception Type:</th>", html)
-        self.assertNotIn("<th>Exception Value:</th>", html)
+        self.assertIn('<th scope="row">Request Method:</th>', html)
+        self.assertIn('<th scope="row">Request URL:</th>', html)
+        self.assertNotIn('<th scope="row">Exception Type:</th>', html)
+        self.assertNotIn('<th scope="row">Exception Value:</th>', html)
         self.assertIn("<h2>Traceback ", html)
         self.assertIn("<h2>Request information</h2>", html)
         self.assertNotIn("<p>Request data not supplied</p>", html)
@@ -996,10 +1009,10 @@ class ExceptionReporterTests(SimpleTestCase):
         self.assertIn(
             '<pre class="exception_value">I&#x27;m a little teapot</pre>', html
         )
-        self.assertNotIn("<th>Request Method:</th>", html)
-        self.assertNotIn("<th>Request URL:</th>", html)
-        self.assertNotIn("<th>Exception Type:</th>", html)
-        self.assertNotIn("<th>Exception Value:</th>", html)
+        self.assertNotIn('<th scope="row">Request Method:</th>', html)
+        self.assertNotIn('<th scope="row">Request URL:</th>', html)
+        self.assertNotIn('<th scope="row">Exception Type:</th>', html)
+        self.assertNotIn('<th scope="row">Exception Value:</th>', html)
         self.assertIn("<h2>Traceback ", html)
         self.assertIn("<h2>Request information</h2>", html)
         self.assertIn("<p>Request data not supplied</p>", html)