Skip to content

Commit 933909a

Browse files
authored
Standardize error template paths (#11494)
* Default error view drop top level errors and split proxito error views View changes to support just two paths for errors, `errors/dashboard` and `errors/proxito`. * Replace one off teapot view * Move spam template to standard location * Point proxito views at new errors/proxito path * Move top level errors to dashboard error templates path * Add proxito error templates path with links to dashboard templates for now * Move proxito 404 errors to proxito error path * Make error view handler more explicit Instead of using the status code to find the error template in the error view handler, just specify a template name. This doesn't return the corresponding HTTP status code with the error from the debug error view. * Reword DNS exception reason "Matching DNS record not found" is not as clear as "Domain not found". * Add missing template * Update pattern for error view for proxito Don't try to default the status code in the view, but instead leave this alone and let the template decide. This allows for 4xx/5xx fallback templates to state the actual status code. * Fix URL load order bug
1 parent 1af71fe commit 933909a

40 files changed

+214
-112
lines changed

readthedocs/core/views/__init__.py

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class ErrorView(TemplateView):
9393
multiple subpaths for errors, as we need to show application themed errors
9494
for dashboard users and minimal error pages for documentation readers.
9595
96+
Template resolution also uses fallback to generic 4xx/5xx error templates.
97+
9698
View arguments:
9799
98100
status_code
@@ -105,49 +107,38 @@ class ErrorView(TemplateView):
105107
separate path from Proxito error templates.
106108
"""
107109

108-
base_path = "errors/dashboard/"
109-
status_code = 500
110+
base_path = "errors/dashboard"
111+
status_code = None
112+
template_name = None
110113

111114
def get_status_code(self):
112-
status_code = self.status_code
113-
try:
114-
status_code = int(self.kwargs["status_code"])
115-
except (ValueError, KeyError):
116-
pass
117-
return status_code
115+
return self.kwargs.get("status_code", self.status_code)
116+
117+
def get_template_name(self):
118+
return self.kwargs.get("template_name", self.template_name)
118119

119120
def get_template_names(self):
120-
status_code = self.get_status_code()
121-
if settings.RTD_EXT_THEME_ENABLED:
122-
# First try to load the template for the specific HTTP status code
123-
# and fall back to a generic 400/500 level error template
124-
status_code_class = int(status_code / 100)
125-
generic_code = f"{status_code_class}xx"
126-
return [
127-
f"{self.base_path}/{code}.html" for code in [status_code, generic_code]
128-
]
129-
# TODO the legacy dashboard has top level path errors, as is the
130-
# default. This can be removed later.
131-
return f"{status_code}.html"
121+
template_names = []
122+
if (template_name := self.get_template_name()) is not None:
123+
template_names.append(template_name.rstrip("/"))
124+
if (status_code := self.get_status_code()) is not None:
125+
template_names.append(str(status_code))
126+
return [f"{self.base_path}/{file}.html" for file in template_names]
127+
128+
def get_context_data(self, **kwargs):
129+
context_data = super().get_context_data(**kwargs)
130+
context_data["status_code"] = self.get_status_code()
131+
return context_data
132132

133133
def dispatch(self, request, *args, **kwargs):
134-
context = self.get_context_data(**kwargs)
134+
context = self.get_context_data()
135135
status_code = self.get_status_code()
136136
return self.render_to_response(
137137
context,
138138
status=status_code,
139139
)
140140

141141

142-
# TODO replace this with ErrorView and a template in `errors/` instead
143-
class TeapotView(TemplateView):
144-
template_name = "core/teapot.html"
145-
146-
def get(self, request, *args, **kwargs):
147-
context = self.get_context_data(**kwargs)
148-
return self.render_to_response(context, status=418)
149-
150-
151142
class PageNotFoundView(View):
152143

153144
"""Just a 404 view that ignores all URL parameters."""

readthedocs/projects/views/base.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,7 @@ def get(self, request, *args, **kwargs):
107107
)
108108

109109
if is_show_dashboard_denied(self.get_project()):
110-
template_name = "spam.html"
111-
if settings.RTD_EXT_THEME_ENABLED:
112-
template_name = "errors/dashboard/410.html"
110+
template_name = "errors/dashboard/spam.html"
113111
return render(request, template_name=template_name, status=410)
114112

115113
return super().get(request, *args, **kwargs)

readthedocs/proxito/exceptions.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ContextualizedHttp404(Http404):
1919
The contextualized exception is handled by proxito's 404 handler
2020
"""
2121

22-
template_name = "errors/404/base.html"
22+
template_name = "errors/proxito/404/base.html"
2323
not_found_subject = pgettext_lazy(_not_found_subject_translation_context, "page")
2424

2525
def __init__(self, http_status=404, path_not_found=None, **kwargs):
@@ -48,10 +48,8 @@ class DomainDNSHttp404(ContextualizedHttp404):
4848

4949
"""Raised if a DNS record points to us and we don't know the domain."""
5050

51-
template_name = "errors/404/dns.html"
52-
not_found_subject = pgettext_lazy(
53-
_not_found_subject_translation_context, "matching DNS record"
54-
)
51+
template_name = "errors/proxito/404/dns.html"
52+
not_found_subject = pgettext_lazy(_not_found_subject_translation_context, "domain")
5553

5654
def __init__(self, domain, **kwargs):
5755
"""
@@ -73,7 +71,7 @@ class ProjectHttp404(ContextualizedHttp404):
7371
It indicates a number of reasons for the user.
7472
"""
7573

76-
template_name = "errors/404/no_project.html"
74+
template_name = "errors/proxito/404/no_project.html"
7775
not_found_subject = pgettext_lazy(_not_found_subject_translation_context, "project")
7876

7977
def __init__(self, domain, **kwargs):
@@ -91,7 +89,7 @@ class SubprojectHttp404(ContextualizedHttp404):
9189

9290
"""Raised if a subproject was not found."""
9391

94-
template_name = "errors/404/no_subproject.html"
92+
template_name = "errors/proxito/404/no_subproject.html"
9593
not_found_subject = pgettext_lazy(
9694
"Names an object not found in a 404 error", "subproject"
9795
)
@@ -111,7 +109,7 @@ class ProjectFilenameHttp404(ContextualizedHttp404):
111109

112110
"""Raised if a page inside an existing project was not found."""
113111

114-
template_name = "errors/404/no_project_page.html"
112+
template_name = "errors/proxito/404/no_project_page.html"
115113
not_found_subject = pgettext_lazy(
116114
_not_found_subject_translation_context, "documentation page"
117115
)
@@ -136,7 +134,7 @@ class ProjectTranslationHttp404(ContextualizedHttp404):
136134
If a page isn't found, raise a ProjectPageHttp404.
137135
"""
138136

139-
template_name = "errors/404/no_language.html"
137+
template_name = "errors/proxito/404/no_language.html"
140138
not_found_subject = pgettext_lazy(
141139
"Names an object not found in a 404 error", "translation"
142140
)
@@ -160,7 +158,7 @@ class ProjectVersionHttp404(ContextualizedHttp404):
160158
Note: The containing project can be a subproject.
161159
"""
162160

163-
template_name = "errors/404/no_version.html"
161+
template_name = "errors/proxito/404/no_version.html"
164162
not_found_subject = pgettext_lazy(
165163
_not_found_subject_translation_context, "documentation version"
166164
)

readthedocs/proxito/urls.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@
3333
pip.rtd.io/_/api/*
3434
"""
3535

36+
from functools import reduce
37+
from operator import add
38+
3639
from django.conf import settings
3740
from django.urls import include, path, re_path
38-
from django.views import defaults
3941

4042
from readthedocs.constants import pattern_opts
41-
from readthedocs.core.views import HealthCheckView, TeapotView
43+
from readthedocs.core.views import HealthCheckView
4244
from readthedocs.projects.views.public import ProjectDownloadMedia
4345
from readthedocs.proxito.views.hosting import ReadTheDocsConfigJson
4446
from readthedocs.proxito.views.serve import (
@@ -49,7 +51,7 @@
4951
ServeSitemapXML,
5052
ServeStaticFiles,
5153
)
52-
from readthedocs.proxito.views.utils import proxito_404_page_handler
54+
from readthedocs.proxito.views.utils import ProxitoErrorView, proxito_404_page_handler
5355

5456
DOC_PATH_PREFIX = getattr(settings, "DOC_PATH_PREFIX", "")
5557

@@ -154,19 +156,19 @@
154156
# /projects/<project_slug>/
155157
re_path(
156158
r"^projects/(?P<project_slug>{project_slug})/$".format(**pattern_opts),
157-
TeapotView.as_view(),
159+
ProxitoErrorView.as_view(status_code=418),
158160
name="projects_detail",
159161
),
160162
# /projects/<project_slug>/builds/
161163
re_path(
162164
(r"^projects/(?P<project_slug>{project_slug})/builds/$".format(**pattern_opts)),
163-
TeapotView.as_view(),
165+
ProxitoErrorView.as_view(status_code=418),
164166
name="builds_project_list",
165167
),
166168
# /projects/<project_slug>/versions/
167169
re_path(
168170
r"^projects/(?P<project_slug>{project_slug})/versions/$".format(**pattern_opts),
169-
TeapotView.as_view(),
171+
ProxitoErrorView.as_view(status_code=418),
170172
name="project_version_list",
171173
),
172174
# /projects/<project_slug>/downloads/
@@ -176,7 +178,7 @@
176178
**pattern_opts
177179
)
178180
),
179-
TeapotView.as_view(),
181+
ProxitoErrorView.as_view(status_code=418),
180182
name="project_downloads",
181183
),
182184
# /projects/<project_slug>/builds/<build_id>/
@@ -186,21 +188,41 @@
186188
**pattern_opts
187189
)
188190
),
189-
TeapotView.as_view(),
191+
ProxitoErrorView.as_view(status_code=418),
190192
name="builds_detail",
191193
),
192194
# /projects/<project_slug>/version/<version_slug>/
193195
re_path(
194196
r"^projects/(?P<project_slug>[-\w]+)/version/(?P<version_slug>[^/]+)/edit/$",
195-
TeapotView.as_view(),
197+
ProxitoErrorView.as_view(status_code=418),
196198
name="project_version_detail",
197199
),
198200
]
199201

200-
urlpatterns = (
201-
health_check_urls + proxied_urls + core_urls + docs_urls + dummy_dashboard_urls
202-
)
202+
debug_urls = [
203+
# For testing error responses and templates
204+
re_path(
205+
r"^{DOC_PATH_PREFIX}error/(?P<template_name>.*)$".format(
206+
DOC_PATH_PREFIX=DOC_PATH_PREFIX,
207+
),
208+
ProxitoErrorView.as_view(),
209+
),
210+
]
211+
212+
groups = [
213+
health_check_urls,
214+
proxied_urls,
215+
core_urls,
216+
docs_urls,
217+
# Fallback paths only required for resolving URLs, evaluate these last
218+
dummy_dashboard_urls,
219+
]
220+
221+
if settings.SHOW_DEBUG_TOOLBAR:
222+
groups.insert(0, debug_urls)
223+
224+
urlpatterns = reduce(add, groups)
203225

204226
# Use Django default error handlers to make things simpler
205227
handler404 = proxito_404_page_handler
206-
handler500 = defaults.server_error
228+
handler500 = ProxitoErrorView.as_view(status_code=500)

readthedocs/proxito/views/mixins.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,9 @@ def _spam_response(self, request, project):
272272
from readthedocsext.spamfighting.utils import is_serve_docs_denied # noqa
273273

274274
if is_serve_docs_denied(project):
275-
return render(request, template_name="spam.html", status=410)
275+
return render(
276+
request, template_name="errors/proxito/spam.html", status=410
277+
)
276278

277279

278280
class ServeRedirectMixin:

readthedocs/proxito/views/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22
from django.http import HttpResponse
33
from django.shortcuts import render
44

5+
from readthedocs.core.views import ErrorView
6+
57
from ..exceptions import ContextualizedHttp404
68

79
log = structlog.get_logger(__name__) # noqa
810

911

12+
class ProxitoErrorView(ErrorView):
13+
base_path = "errors/proxito"
14+
15+
1016
def fast_404(request, *args, **kwargs):
1117
"""
1218
A fast error page handler.
@@ -18,7 +24,7 @@ def fast_404(request, *args, **kwargs):
1824

1925

2026
def proxito_404_page_handler(
21-
request, template_name="errors/404/base.html", exception=None
27+
request, template_name="errors/proxito/404/base.html", exception=None
2228
):
2329
"""
2430
Serves a 404 error message, handling 404 exception types raised throughout the app.

readthedocs/templates/404.html

Lines changed: 0 additions & 2 deletions
This file was deleted.

readthedocs/templates/errors/404/no_language.html

Lines changed: 0 additions & 10 deletions
This file was deleted.

readthedocs/templates/errors/404/no_project_page.html

Lines changed: 0 additions & 10 deletions
This file was deleted.

readthedocs/templates/errors/404/no_subproject.html

Lines changed: 0 additions & 10 deletions
This file was deleted.

readthedocs/templates/errors/404/no_version.html

Lines changed: 0 additions & 10 deletions
This file was deleted.

readthedocs/templates/401.html renamed to readthedocs/templates/errors/dashboard/401.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% extends "errors/base.html" %}
1+
{% extends "errors/dashboard/base.html" %}
22
{% load core_tags %}
33
{% load i18n %}
44

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{% extends "errors/dashboard/base.html" %}
2+
{% load core_tags %}
3+
{% load i18n %}
4+
5+
{% block title %}
6+
{% trans "404 Not Found" %}
7+
{% endblock %}
8+
9+
{% block content %}
10+
11+
<h1>{% trans "404 Not Found" %}</h1>
12+
13+
{% block 404_error_message %}
14+
<section>
15+
<p>{% trans "You have encountered a 404 on Read the Docs. You are either looking for a page that does not exist or a project that has been removed." %}</p>
16+
</section>
17+
18+
<pre style="line-height: 1.25; white-space: pre;">
19+
20+
\ What a maze! /
21+
\ /
22+
\ This page does /
23+
] not exist. [ ,'|
24+
] [ / |
25+
]___ ___[ ,' |
26+
] ]\ /[ [ |: |
27+
] ] \ / [ [ |: |
28+
] ] ] [ [ [ |: |
29+
] ] ]__ __[ [ [ |: |
30+
] ] ] ]\ _ /[ [ [ [ |: |
31+
] ] ] ] (#) [ [ [ [ :===='
32+
] ] ]_].nHn.[_[ [ [
33+
] ] ] HHHHH. [ [ [
34+
] ] / `HH("N \ [ [
35+
]__]/ HHH " \[__[
36+
] NNN [
37+
] N/" [
38+
] N H [
39+
/ N \
40+
/ q, \
41+
/ \
42+
</pre>
43+
44+
{% endblock %}
45+
46+
{% endblock %}

0 commit comments

Comments
 (0)