Skip to content

Notifications: use Template's Django engine to render them #11024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 27 additions & 27 deletions readthedocs/config/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
body=_(
textwrap.dedent(
"""
Configuration file not found in <code>{directory}</code>.
Configuration file not found in <code>{{directory}}</code>.
"""
).strip(),
),
Expand All @@ -53,7 +53,7 @@
body=_(
textwrap.dedent(
"""
The <code>{key}</code> configuration option is not supported in this version.
The <code>{{key}}</code> configuration option is not supported in this version.
"""
).strip(),
),
Expand Down Expand Up @@ -105,11 +105,11 @@
body=_(
textwrap.dedent(
"""
Invalid configuration option: <code>{key}</code>.
Invalid configuration option: <code>{{key}}</code>.

Read the Docs configuration file: <code>{source_file}</code>.
Read the Docs configuration file: <code>{{source_file}}</code>.

<code>{error_message}</code>
<code>{{error_message}}</code>
"""
).strip(),
),
Expand Down Expand Up @@ -145,7 +145,7 @@
body=_(
textwrap.dedent(
"""
The name of the packages (e.g. <code>{package}</code>) can't start with <code>{prefix}</code>
The name of the packages (e.g. <code>{{package}}</code>) can't start with <code>{{prefix}}</code>
"""
).strip(),
),
Expand All @@ -157,7 +157,7 @@
body=_(
textwrap.dedent(
"""
The name of the package <code>{pacakge}</name> is invalid.
The name of the package <code>{{pacakge}}</name> is invalid.
"""
).strip(),
),
Expand Down Expand Up @@ -213,11 +213,11 @@
),
Message(
id=ConfigError.INVALID_KEY_NAME,
header=_("Invalid configuration key: {key}"),
header=_("Invalid configuration key: {{key}}"),
body=_(
textwrap.dedent(
"""
Make sure the key name <code>{key}</code> is correct.
Make sure the key name <code>{{key}}</code> is correct.
"""
).strip(),
),
Expand All @@ -229,9 +229,9 @@
body=_(
textwrap.dedent(
"""
Error while parsing <code>{filename}</code>.
Error while parsing <code>{{filename}}</code>.

{error_message}
{{error_message}}
"""
).strip(),
),
Expand All @@ -248,8 +248,8 @@
body=_(
textwrap.dedent(
"""
Config validation error in <code>{key}</code>.
Expected one of (0, 1, true, false), got <code>{value}</code>.
Config validation error in <code>{{key}}</code>.
Expected one of (0, 1, true, false), got <code>{{value}}</code>.
"""
).strip(),
),
Expand All @@ -261,8 +261,8 @@
body=_(
textwrap.dedent(
"""
Config validation error in <code>{key}</code>.
Expected one of ({choices}), got <code>{value}</code>.
Config validation error in <code>{{key}}</code>.
Expected one of ({{choices}}), got <code>{{value}}</code>.
"""
).strip(),
),
Expand All @@ -274,8 +274,8 @@
body=_(
textwrap.dedent(
"""
Config validation error in <code>{key}</code>.
Expected a dictionary, got <code>{value}</code>.
Config validation error in <code>{{key}}</code>.
Expected a dictionary, got <code>{{value}}</code>.
"""
).strip(),
),
Expand All @@ -287,8 +287,8 @@
body=_(
textwrap.dedent(
"""
Config validation error in <code>{key}</code>.
The path <code>{value}</code> does not exist.
Config validation error in <code>{{key}}</code>.
The path <code>{{value}}</code> does not exist.
"""
).strip(),
),
Expand All @@ -300,8 +300,8 @@
body=_(
textwrap.dedent(
"""
Config validation error in <code>{key}</code>.
The path <code>{value}</code> is not a valid path pattern.
Config validation error in <code>{{key}}</code>.
The path <code>{{value}}</code> is not a valid path pattern.
"""
).strip(),
),
Expand All @@ -313,8 +313,8 @@
body=_(
textwrap.dedent(
"""
Config validation error in <code>{key}</code>.
Expected a string, got <code>{value}</code>.
Config validation error in <code>{{key}}</code>.
Expected a string, got <code>{{value}}</code>.
"""
).strip(),
),
Expand All @@ -326,8 +326,8 @@
body=_(
textwrap.dedent(
"""
Config validation error in <code>{key}</code>.
Expected a list, got <code>{value}</code>.
Config validation error in <code>{{key}}</code>.
Expected a list, got <code>{{value}}</code>.
"""
).strip(),
),
Expand All @@ -339,8 +339,8 @@
body=_(
textwrap.dedent(
"""
Config validation error in <code>{key}</code>.
Value <code>{value}</code> not found.
Config validation error in <code>{{key}}</code>.
Value <code>{{value}}</code> not found.
"""
).strip(),
),
Expand Down
2 changes: 1 addition & 1 deletion readthedocs/core/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
textwrap.dedent(
"""
Your primary email address is not verified.
Please <a href="{account_email_url}">verify it here</a>.
Please <a href="{{account_email_url}}">verify it here</a>.
"""
).strip(),
),
Expand Down
12 changes: 6 additions & 6 deletions readthedocs/domains/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ class PendingCustomDomainValidation(EmailNotification):
messages = [
Message(
id=MESSAGE_DOMAIN_VALIDATION_PENDING,
header=_("Pending configuration of custom domain: {domain}"),
header=_("Pending configuration of custom domain: {{domain}}"),
body=_(
textwrap.dedent(
"""
The configuration of your custom domain <code>{domain}</code> is pending.
Go to the <a href="{domain_url}">domain page</a> and follow the instructions to complete it.
The configuration of your custom domain <code>{{domain}}</code> is pending.
Go to the <a href="{{domain_url}}">domain page</a> and follow the instructions to complete it.
"""
).strip(),
),
Expand All @@ -36,12 +36,12 @@ class PendingCustomDomainValidation(EmailNotification):
# ``message_id``
Message(
id=MESSAGE_DOMAIN_VALIDATION_EXPIRED,
header=_("Validation of custom domain expired: {domain}"),
header=_("Validation of custom domain expired: {{domain}}"),
body=_(
textwrap.dedent(
"""
The validation period of your custom domain <code>{domain}</code> has ended.
Go to the <a href="{domain_url}">domain page</a> and click on "Save" to restart the process.
The validation period of your custom domain <code>{{domain}}</code> has ended.
Go to the <a href="{{domain_url}}">domain page</a> and click on "Save" to restart the process.
"""
).strip(),
),
Expand Down
61 changes: 17 additions & 44 deletions readthedocs/notifications/messages.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import textwrap
from collections import defaultdict

import structlog
from django.utils.html import escape
from django.template import Context, Template
from django.utils.translation import gettext_noop as _

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

def _escape_format_values(self, format_values):
"""
Escape all potential HTML tags included in format values.

This is a protection against rendering potential values defined by the user.
It uses the Django's util function ``escape`` (similar to ``|escape`` template tag filter)
to convert HTML characters into regular characters.

NOTE: currently, we don't support values that are not ``str`` or ``int``.
If we want to support other types or nested dictionaries,
we will need to iterate recursively to apply the ``escape`` function.
"""
return {
key: escape(value)
for key, value in format_values.items()
if isinstance(value, (str, int))
}

def set_format_values(self, format_values):
self.format_values = self._escape_format_values(format_values)
self.format_values = format_values

def get_display_icon_classes(self):
if self.icon_classes:
Expand All @@ -78,20 +59,12 @@ def get_display_icon_classes(self):
return " ".join(classes)

def get_rendered_header(self):
try:
return self.header.format(**self.format_values)
except KeyError:
# There was a key missing
log.exception("There was a missing key when formating a header's Message.")
return self.header.format_map(defaultdict(str, **self.format_values))
template = Template(self.header)
return template.render(context=Context(self.format_values))

def get_rendered_body(self):
try:
return self.body.format(**self.format_values)
except KeyError:
# There was a key missing
log.exception("There was a missing key when formating a body's Message.")
return self.body.format_map(defaultdict(str, **self.format_values))
template = Template(self.body)
return template.render(context=Context(self.format_values))


# TODO: review the copy of these notifications/messages on PR review and adapt them.
Expand All @@ -109,7 +82,7 @@ def get_rendered_body(self):
There was a problem with Read the Docs while building your documentation.
Please try again later.
If this problem persists,
report this error to us with your build id ({instance.pk}).
report this error to us with your build id ({{instance.pk}}).
"""
).strip(),
),
Expand All @@ -123,7 +96,7 @@ def get_rendered_body(self):
"""
This build was terminated due to inactivity.
If you continue to encounter this error,
file a support request and reference this build id ({instance.pk}).
file a support request and reference this build id ({{instance.pk}}).
"""
).strip(),
),
Expand All @@ -149,7 +122,7 @@ def get_rendered_body(self):
body=_(
textwrap.dedent(
"""
Concurrency limit reached ({limit}), retrying in 5 minutes.
Concurrency limit reached ({{limit}}), retrying in 5 minutes.
"""
).strip(),
),
Expand Down Expand Up @@ -210,7 +183,7 @@ def get_rendered_body(self):
body=_(
textwrap.dedent(
"""
Build exited due to unknown error: {message}
Build exited due to unknown error: {{message}}
"""
).strip(),
),
Expand All @@ -236,7 +209,7 @@ def get_rendered_body(self):
body=_(
textwrap.dedent(
"""
Your project, organization, or user is currently building the maximum concurrency builds allowed ({limit}).
Your project, organization, or user is currently building the maximum concurrency builds allowed ({{limit}}).
It will automatically retry in 5 minutes.
"""
).strip(),
Expand All @@ -261,7 +234,7 @@ def get_rendered_body(self):
body=_(
textwrap.dedent(
"""
Build output directory for format "{artifact_type}" is not a directory.
Build output directory for format "{{artifact_type}}" is not a directory.
"""
).strip(),
),
Expand All @@ -273,7 +246,7 @@ def get_rendered_body(self):
body=_(
textwrap.dedent(
"""
Build output directory for format "{artifact_type}" does not contain any files.
Build output directory for format "{{artifact_type}}" does not contain any files.
It seems the build process created the directory but did not save any file to it.
"""
).strip(),
Expand All @@ -286,9 +259,9 @@ def get_rendered_body(self):
body=_(
textwrap.dedent(
"""
Build output directory for format "{artifact_type}" contains multiple files
Build output directory for format "{{artifact_type}}" contains multiple files
and it is not currently supported.
Please, remove all the files but the "{artifact_type}" you want to upload.
Please, remove all the files but the "{{artifact_type}}" you want to upload.
"""
).strip(),
),
Expand Down Expand Up @@ -421,7 +394,7 @@ def get_rendered_body(self):
body=_(
textwrap.dedent(
"""
Problem parsing MkDocs YAML configuration. {exception}
Problem parsing MkDocs YAML configuration. {{exception}}
"""
).strip(),
),
Expand Down Expand Up @@ -457,7 +430,7 @@ def get_rendered_body(self):
body=_(
textwrap.dedent(
"""
The "{config}" config from your MkDocs YAML config file has to be a
The "{{config}}" config from your MkDocs YAML config file has to be a
list of relative paths.
"""
).strip(),
Expand Down
Loading