Files
Carlo Costino 99edc88197 Localize notification_utils to the API
This changeset pulls in all of the notification_utils code directly into the API and removes it as an external dependency.  We are doing this to cut down on operational maintenance of the project and will begin removing parts of it no longer needed for the API.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-05-16 10:17:45 -04:00

209 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from markupsafe import Markup
from ordered_set import OrderedSet
from notifications_utils.formatters import (
escape_html,
strip_and_remove_obscure_whitespace,
strip_html,
unescaped_formatted_list,
)
from notifications_utils.insensitive_dict import InsensitiveDict
class Placeholder:
def __init__(self, body):
# body shouldnt include leading/trailing brackets, like (( and ))
self.body = body.lstrip("(").rstrip(")")
@classmethod
def from_match(cls, match):
return cls(match.group(0))
def is_conditional(self):
return "??" in self.body
@property
def name(self):
# for non conditionals, name equals body
return self.body.split("??")[0]
@property
def conditional_text(self):
if self.is_conditional():
# ((a?? b??c)) returns " b??c"
return "??".join(self.body.split("??")[1:])
else:
raise ValueError("{} not conditional".format(self))
def get_conditional_body(self, show_conditional):
# note: unsanitised/converted
if self.is_conditional():
return self.conditional_text if str2bool(show_conditional) else ""
else:
raise ValueError("{} not conditional".format(self))
def __repr__(self):
return "Placeholder({})".format(self.body)
class Field:
"""
An instance of Field represents a string of text which may contain
placeholders.
If values are provided the field replaces the placeholders with the
corresponding values. If a value for a placeholder is missing then
the field will highlight the placeholder by wrapping it in some HTML.
A template can have several fields, for example an email template
has a field for the body and a field for the subject.
"""
placeholder_pattern = re.compile(
r"\({2}" # opening ((
r"([^()]+)" # body of placeholder - potentially standard or conditional.
r"\){2}" # closing ))
)
placeholder_tag = "<span class='placeholder'>(({}))</span>"
conditional_placeholder_tag = (
"<span class='placeholder-conditional'>(({}??</span>{}))"
)
placeholder_tag_no_brackets = "<span class='placeholder-no-brackets'>{}</span>"
placeholder_tag_redacted = "<span class='placeholder-redacted'>hidden</span>"
def __init__(
self,
content,
values=None,
with_brackets=True,
html="strip",
markdown_lists=False,
redact_missing_personalisation=False,
):
self.content = content
self.values = values
self.markdown_lists = markdown_lists
if not with_brackets:
self.placeholder_tag = self.placeholder_tag_no_brackets
self.sanitizer = {
"strip": strip_html,
"escape": escape_html,
"passthrough": str,
}[html]
self.redact_missing_personalisation = redact_missing_personalisation
def __str__(self):
if self.values:
return self.replaced
return self.formatted
def __repr__(self):
return '{}("{}", {})'.format(
self.__class__.__name__, self.content, self.values
) # TODO: more real
def splitlines(self):
return str(self).splitlines()
@property
def values(self):
return self._values
@values.setter
def values(self, value):
self._values = InsensitiveDict(value) if value else {}
def format_match(self, match):
placeholder = Placeholder.from_match(match)
if self.redact_missing_personalisation:
return self.placeholder_tag_redacted
if placeholder.is_conditional():
return self.conditional_placeholder_tag.format(
placeholder.name, placeholder.conditional_text
)
return self.placeholder_tag.format(placeholder.name)
def replace_match(self, match):
placeholder = Placeholder.from_match(match)
replacement = self.values.get(placeholder.name)
if placeholder.is_conditional() and replacement is not None:
return placeholder.get_conditional_body(replacement)
replaced_value = self.get_replacement(placeholder)
if replaced_value is not None:
return self.get_replacement(placeholder)
return self.format_match(match)
def get_replacement(self, placeholder):
replacement = self.values.get(placeholder.name)
if replacement is None:
return None
if isinstance(replacement, list):
vals = (
strip_and_remove_obscure_whitespace(str(val))
for val in replacement
if val is not None
)
vals = list(filter(None, vals))
if not vals:
return ""
return self.sanitizer(self.get_replacement_as_list(vals))
return self.sanitizer(str(replacement))
def get_replacement_as_list(self, replacement):
if self.markdown_lists:
return "\n\n" + "\n".join("* {}".format(item) for item in replacement)
return unescaped_formatted_list(replacement, before_each="", after_each="")
@property
def _raw_formatted(self):
return re.sub(
self.placeholder_pattern, self.format_match, self.sanitizer(self.content)
)
@property
def formatted(self):
return Markup(self._raw_formatted)
@property
def placeholders(self):
if not getattr(self, "content", ""):
return set()
return OrderedSet(
Placeholder(body).name
for body in re.findall(self.placeholder_pattern, self.content)
)
@property
def replaced(self):
return re.sub(
self.placeholder_pattern, self.replace_match, self.sanitizer(self.content)
)
class PlainTextField(Field):
"""
Use this where no HTML should be rendered in the outputted content,
even when no values have been passed in
"""
placeholder_tag = "(({}))"
conditional_placeholder_tag = "(({}??{}))"
placeholder_tag_no_brackets = "{}"
placeholder_tag_redacted = "[hidden]"
def str2bool(value):
if not value:
return False
return str(value).lower() in ("yes", "y", "true", "t", "1", "include", "show")