Skip to content

Commit 34ccbad

Browse files
authored
Notifications: use Template's Django engine to render them (#11024)
* Notifications: use `Template`'s Django engine to render them We dealt a few times when rendering notifications using simple Python's `.format()` and we changed our mind to use Django's engine instead. Closes #11022 * Revert #11018 and use `instance.name` again * Update tests
1 parent 74fa96c commit 34ccbad

File tree

8 files changed

+71
-117
lines changed

8 files changed

+71
-117
lines changed

readthedocs/config/notifications.py

+27-27
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
body=_(
4242
textwrap.dedent(
4343
"""
44-
Configuration file not found in <code>{directory}</code>.
44+
Configuration file not found in <code>{{directory}}</code>.
4545
"""
4646
).strip(),
4747
),
@@ -53,7 +53,7 @@
5353
body=_(
5454
textwrap.dedent(
5555
"""
56-
The <code>{key}</code> configuration option is not supported in this version.
56+
The <code>{{key}}</code> configuration option is not supported in this version.
5757
"""
5858
).strip(),
5959
),
@@ -105,11 +105,11 @@
105105
body=_(
106106
textwrap.dedent(
107107
"""
108-
Invalid configuration option: <code>{key}</code>.
108+
Invalid configuration option: <code>{{key}}</code>.
109109
110-
Read the Docs configuration file: <code>{source_file}</code>.
110+
Read the Docs configuration file: <code>{{source_file}}</code>.
111111
112-
<code>{error_message}</code>
112+
<code>{{error_message}}</code>
113113
"""
114114
).strip(),
115115
),
@@ -145,7 +145,7 @@
145145
body=_(
146146
textwrap.dedent(
147147
"""
148-
The name of the packages (e.g. <code>{package}</code>) can't start with <code>{prefix}</code>
148+
The name of the packages (e.g. <code>{{package}}</code>) can't start with <code>{{prefix}}</code>
149149
"""
150150
).strip(),
151151
),
@@ -157,7 +157,7 @@
157157
body=_(
158158
textwrap.dedent(
159159
"""
160-
The name of the package <code>{pacakge}</name> is invalid.
160+
The name of the package <code>{{pacakge}}</name> is invalid.
161161
"""
162162
).strip(),
163163
),
@@ -213,11 +213,11 @@
213213
),
214214
Message(
215215
id=ConfigError.INVALID_KEY_NAME,
216-
header=_("Invalid configuration key: {key}"),
216+
header=_("Invalid configuration key: {{key}}"),
217217
body=_(
218218
textwrap.dedent(
219219
"""
220-
Make sure the key name <code>{key}</code> is correct.
220+
Make sure the key name <code>{{key}}</code> is correct.
221221
"""
222222
).strip(),
223223
),
@@ -229,9 +229,9 @@
229229
body=_(
230230
textwrap.dedent(
231231
"""
232-
Error while parsing <code>{filename}</code>.
232+
Error while parsing <code>{{filename}}</code>.
233233
234-
{error_message}
234+
{{error_message}}
235235
"""
236236
).strip(),
237237
),
@@ -248,8 +248,8 @@
248248
body=_(
249249
textwrap.dedent(
250250
"""
251-
Config validation error in <code>{key}</code>.
252-
Expected one of (0, 1, true, false), got <code>{value}</code>.
251+
Config validation error in <code>{{key}}</code>.
252+
Expected one of (0, 1, true, false), got <code>{{value}}</code>.
253253
"""
254254
).strip(),
255255
),
@@ -261,8 +261,8 @@
261261
body=_(
262262
textwrap.dedent(
263263
"""
264-
Config validation error in <code>{key}</code>.
265-
Expected one of ({choices}), got <code>{value}</code>.
264+
Config validation error in <code>{{key}}</code>.
265+
Expected one of ({{choices}}), got <code>{{value}}</code>.
266266
"""
267267
).strip(),
268268
),
@@ -274,8 +274,8 @@
274274
body=_(
275275
textwrap.dedent(
276276
"""
277-
Config validation error in <code>{key}</code>.
278-
Expected a dictionary, got <code>{value}</code>.
277+
Config validation error in <code>{{key}}</code>.
278+
Expected a dictionary, got <code>{{value}}</code>.
279279
"""
280280
).strip(),
281281
),
@@ -287,8 +287,8 @@
287287
body=_(
288288
textwrap.dedent(
289289
"""
290-
Config validation error in <code>{key}</code>.
291-
The path <code>{value}</code> does not exist.
290+
Config validation error in <code>{{key}}</code>.
291+
The path <code>{{value}}</code> does not exist.
292292
"""
293293
).strip(),
294294
),
@@ -300,8 +300,8 @@
300300
body=_(
301301
textwrap.dedent(
302302
"""
303-
Config validation error in <code>{key}</code>.
304-
The path <code>{value}</code> is not a valid path pattern.
303+
Config validation error in <code>{{key}}</code>.
304+
The path <code>{{value}}</code> is not a valid path pattern.
305305
"""
306306
).strip(),
307307
),
@@ -313,8 +313,8 @@
313313
body=_(
314314
textwrap.dedent(
315315
"""
316-
Config validation error in <code>{key}</code>.
317-
Expected a string, got <code>{value}</code>.
316+
Config validation error in <code>{{key}}</code>.
317+
Expected a string, got <code>{{value}}</code>.
318318
"""
319319
).strip(),
320320
),
@@ -326,8 +326,8 @@
326326
body=_(
327327
textwrap.dedent(
328328
"""
329-
Config validation error in <code>{key}</code>.
330-
Expected a list, got <code>{value}</code>.
329+
Config validation error in <code>{{key}}</code>.
330+
Expected a list, got <code>{{value}}</code>.
331331
"""
332332
).strip(),
333333
),
@@ -339,8 +339,8 @@
339339
body=_(
340340
textwrap.dedent(
341341
"""
342-
Config validation error in <code>{key}</code>.
343-
Value <code>{value}</code> not found.
342+
Config validation error in <code>{{key}}</code>.
343+
Value <code>{{value}}</code> not found.
344344
"""
345345
).strip(),
346346
),

readthedocs/core/notifications.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
textwrap.dedent(
1717
"""
1818
Your primary email address is not verified.
19-
Please <a href="{account_email_url}">verify it here</a>.
19+
Please <a href="{{account_email_url}}">verify it here</a>.
2020
"""
2121
).strip(),
2222
),

readthedocs/domains/notifications.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ class PendingCustomDomainValidation(EmailNotification):
2020
messages = [
2121
Message(
2222
id=MESSAGE_DOMAIN_VALIDATION_PENDING,
23-
header=_("Pending configuration of custom domain: {domain}"),
23+
header=_("Pending configuration of custom domain: {{domain}}"),
2424
body=_(
2525
textwrap.dedent(
2626
"""
27-
The configuration of your custom domain <code>{domain}</code> is pending.
28-
Go to the <a href="{domain_url}">domain page</a> and follow the instructions to complete it.
27+
The configuration of your custom domain <code>{{domain}}</code> is pending.
28+
Go to the <a href="{{domain_url}}">domain page</a> and follow the instructions to complete it.
2929
"""
3030
).strip(),
3131
),
@@ -36,12 +36,12 @@ class PendingCustomDomainValidation(EmailNotification):
3636
# ``message_id``
3737
Message(
3838
id=MESSAGE_DOMAIN_VALIDATION_EXPIRED,
39-
header=_("Validation of custom domain expired: {domain}"),
39+
header=_("Validation of custom domain expired: {{domain}}"),
4040
body=_(
4141
textwrap.dedent(
4242
"""
43-
The validation period of your custom domain <code>{domain}</code> has ended.
44-
Go to the <a href="{domain_url}">domain page</a> and click on "Save" to restart the process.
43+
The validation period of your custom domain <code>{{domain}}</code> has ended.
44+
Go to the <a href="{{domain_url}}">domain page</a> and click on "Save" to restart the process.
4545
"""
4646
).strip(),
4747
),

readthedocs/notifications/messages.py

+17-44
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import textwrap
2-
from collections import defaultdict
32

43
import structlog
5-
from django.utils.html import escape
4+
from django.template import Context, Template
65
from django.utils.translation import gettext_noop as _
76

87
from readthedocs.doc_builder.exceptions import (
@@ -34,26 +33,8 @@ def __repr__(self):
3433
def __str__(self):
3534
return f"Message: {self.id} | {self.header}"
3635

37-
def _escape_format_values(self, format_values):
38-
"""
39-
Escape all potential HTML tags included in format values.
40-
41-
This is a protection against rendering potential values defined by the user.
42-
It uses the Django's util function ``escape`` (similar to ``|escape`` template tag filter)
43-
to convert HTML characters into regular characters.
44-
45-
NOTE: currently, we don't support values that are not ``str`` or ``int``.
46-
If we want to support other types or nested dictionaries,
47-
we will need to iterate recursively to apply the ``escape`` function.
48-
"""
49-
return {
50-
key: escape(value)
51-
for key, value in format_values.items()
52-
if isinstance(value, (str, int))
53-
}
54-
5536
def set_format_values(self, format_values):
56-
self.format_values = self._escape_format_values(format_values)
37+
self.format_values = format_values
5738

5839
def get_display_icon_classes(self):
5940
if self.icon_classes:
@@ -78,20 +59,12 @@ def get_display_icon_classes(self):
7859
return " ".join(classes)
7960

8061
def get_rendered_header(self):
81-
try:
82-
return self.header.format(**self.format_values)
83-
except KeyError:
84-
# There was a key missing
85-
log.exception("There was a missing key when formating a header's Message.")
86-
return self.header.format_map(defaultdict(str, **self.format_values))
62+
template = Template(self.header)
63+
return template.render(context=Context(self.format_values))
8764

8865
def get_rendered_body(self):
89-
try:
90-
return self.body.format(**self.format_values)
91-
except KeyError:
92-
# There was a key missing
93-
log.exception("There was a missing key when formating a body's Message.")
94-
return self.body.format_map(defaultdict(str, **self.format_values))
66+
template = Template(self.body)
67+
return template.render(context=Context(self.format_values))
9568

9669

9770
# TODO: review the copy of these notifications/messages on PR review and adapt them.
@@ -109,7 +82,7 @@ def get_rendered_body(self):
10982
There was a problem with Read the Docs while building your documentation.
11083
Please try again later.
11184
If this problem persists,
112-
report this error to us with your build id ({instance.pk}).
85+
report this error to us with your build id ({{instance.pk}}).
11386
"""
11487
).strip(),
11588
),
@@ -123,7 +96,7 @@ def get_rendered_body(self):
12396
"""
12497
This build was terminated due to inactivity.
12598
If you continue to encounter this error,
126-
file a support request and reference this build id ({instance.pk}).
99+
file a support request and reference this build id ({{instance.pk}}).
127100
"""
128101
).strip(),
129102
),
@@ -149,7 +122,7 @@ def get_rendered_body(self):
149122
body=_(
150123
textwrap.dedent(
151124
"""
152-
Concurrency limit reached ({limit}), retrying in 5 minutes.
125+
Concurrency limit reached ({{limit}}), retrying in 5 minutes.
153126
"""
154127
).strip(),
155128
),
@@ -210,7 +183,7 @@ def get_rendered_body(self):
210183
body=_(
211184
textwrap.dedent(
212185
"""
213-
Build exited due to unknown error: {message}
186+
Build exited due to unknown error: {{message}}
214187
"""
215188
).strip(),
216189
),
@@ -236,7 +209,7 @@ def get_rendered_body(self):
236209
body=_(
237210
textwrap.dedent(
238211
"""
239-
Your project, organization, or user is currently building the maximum concurrency builds allowed ({limit}).
212+
Your project, organization, or user is currently building the maximum concurrency builds allowed ({{limit}}).
240213
It will automatically retry in 5 minutes.
241214
"""
242215
).strip(),
@@ -261,7 +234,7 @@ def get_rendered_body(self):
261234
body=_(
262235
textwrap.dedent(
263236
"""
264-
Build output directory for format "{artifact_type}" is not a directory.
237+
Build output directory for format "{{artifact_type}}" is not a directory.
265238
"""
266239
).strip(),
267240
),
@@ -273,7 +246,7 @@ def get_rendered_body(self):
273246
body=_(
274247
textwrap.dedent(
275248
"""
276-
Build output directory for format "{artifact_type}" does not contain any files.
249+
Build output directory for format "{{artifact_type}}" does not contain any files.
277250
It seems the build process created the directory but did not save any file to it.
278251
"""
279252
).strip(),
@@ -286,9 +259,9 @@ def get_rendered_body(self):
286259
body=_(
287260
textwrap.dedent(
288261
"""
289-
Build output directory for format "{artifact_type}" contains multiple files
262+
Build output directory for format "{{artifact_type}}" contains multiple files
290263
and it is not currently supported.
291-
Please, remove all the files but the "{artifact_type}" you want to upload.
264+
Please, remove all the files but the "{{artifact_type}}" you want to upload.
292265
"""
293266
).strip(),
294267
),
@@ -421,7 +394,7 @@ def get_rendered_body(self):
421394
body=_(
422395
textwrap.dedent(
423396
"""
424-
Problem parsing MkDocs YAML configuration. {exception}
397+
Problem parsing MkDocs YAML configuration. {{exception}}
425398
"""
426399
).strip(),
427400
),
@@ -457,7 +430,7 @@ def get_rendered_body(self):
457430
body=_(
458431
textwrap.dedent(
459432
"""
460-
The "{config}" config from your MkDocs YAML config file has to be a
433+
The "{{config}}" config from your MkDocs YAML config file has to be a
461434
list of relative paths.
462435
"""
463436
).strip(),

0 commit comments

Comments
 (0)