Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Allow using several custom template directories #10587

Merged
merged 10 commits into from
Aug 17, 2021
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
1 change: 1 addition & 0 deletions changelog.d/10587.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow multiple custom directories in `read_templates`.
43 changes: 25 additions & 18 deletions synapse/config/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,43 +237,50 @@ def read_template(self, filename: str) -> jinja2.Template:
def read_templates(
self,
filenames: List[str],
custom_template_directory: Optional[str] = None,
custom_template_directories: Optional[Iterable[str]] = None,
) -> List[jinja2.Template]:
"""Load a list of template files from disk using the given variables.

This function will attempt to load the given templates from the default Synapse
template directory. If `custom_template_directory` is supplied, that directory
is tried first.
template directory. If `custom_template_directories` is supplied, any directory
in this list is tried (in the order they appear in the list) before trying
Synapse's default directory.

Files read are treated as Jinja templates. The templates are not rendered yet
and have autoescape enabled.

Args:
filenames: A list of template filenames to read.

custom_template_directory: A directory to try to look for the templates
before using the default Synapse template directory instead.
custom_template_directories: A list of directory to try to look for the
templates before using the default Synapse template directory instead.

Raises:
ConfigError: if the file's path is incorrect or otherwise cannot be read.

Returns:
A list of jinja2 templates.
"""
search_directories = [self.default_template_dir]

# The loader will first look in the custom template directory (if specified) for the
# given filename. If it doesn't find it, it will use the default template dir instead
if custom_template_directory:
# Check that the given template directory exists
if not self.path_exists(custom_template_directory):
raise ConfigError(
"Configured template directory does not exist: %s"
% (custom_template_directory,)
)
search_directories = []

# The loader will first look in the custom template directories (if specified)
# for the given filename. If it doesn't find it, it will use the default
# template dir instead.
if custom_template_directories is not None:
for custom_template_directory in custom_template_directories:
# Check that the given template directory exists
if not self.path_exists(custom_template_directory):
raise ConfigError(
"Configured template directory does not exist: %s"
% (custom_template_directory,)
)

# Search the custom template directory as well
search_directories.append(custom_template_directory)

# Search the custom template directory as well
search_directories.insert(0, custom_template_directory)
# Append the default directory at the end of the list so Jinja can fallback on it
# if a template is missing from any custom directory.
search_directories.append(self.default_template_dir)

# TODO: switch to synapse.util.templates.build_jinja_env
loader = jinja2.FileSystemLoader(search_directories)
Expand Down
2 changes: 1 addition & 1 deletion synapse/config/account_validity.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,5 @@ def read_config(self, config, **kwargs):
"account_previously_renewed.html",
invalid_token_template_filename,
],
account_validity_template_dir,
(td for td in (account_validity_template_dir,) if td),
)
8 changes: 5 additions & 3 deletions synapse/config/emailconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ def read_config(self, config, **kwargs):
registration_template_success_html,
add_threepid_template_success_html,
],
template_dir,
(
td for td in (template_dir,) if td
), # Filter out template_dir if not provided
)

# Render templates that do not contain any placeholders
Expand Down Expand Up @@ -297,7 +299,7 @@ def read_config(self, config, **kwargs):
self.email_notif_template_text,
) = self.read_templates(
[notif_template_html, notif_template_text],
template_dir,
(td for td in (template_dir,) if td),
)

self.email_notif_for_new_users = email_config.get(
Expand All @@ -320,7 +322,7 @@ def read_config(self, config, **kwargs):
self.account_validity_template_text,
) = self.read_templates(
[expiry_template_html, expiry_template_text],
template_dir,
(td for td in (template_dir,) if td),
)

subjects_config = email_config.get("subjects", {})
Expand Down
2 changes: 1 addition & 1 deletion synapse/config/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def read_config(self, config, **kwargs):
"sso_auth_success.html",
"sso_auth_bad_user.html",
],
self.sso_template_dir,
(td for td in (self.sso_template_dir,) if td),
)

# These templates have no placeholders, so render them here
Expand Down
5 changes: 4 additions & 1 deletion synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,10 @@ def read_templates(
A list containing the loaded templates, with the orders matching the one of
the filenames parameter.
"""
return self._hs.config.read_templates(filenames, custom_template_directory)
return self._hs.config.read_templates(
filenames,
(td for td in (custom_template_directory,) if td),
)


class PublicRoomListManager:
Expand Down
64 changes: 61 additions & 3 deletions tests/config/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_loading_missing_templates(self):
# contain template files
with tempfile.TemporaryDirectory() as tmp_dir:
# Attempt to load an HTML template from our custom template directory
template = self.hs.config.read_templates(["sso_error.html"], tmp_dir)[0]
template = self.hs.config.read_templates(["sso_error.html"], (tmp_dir,))[0]

# If no errors, we should've gotten the default template instead

Expand Down Expand Up @@ -60,7 +60,7 @@ def test_loading_custom_templates(self):

# Attempt to load the template from our custom template directory
template = (
self.hs.config.read_templates([template_filename], tmp_dir)
self.hs.config.read_templates([template_filename], (tmp_dir,))
)[0]

# Render the template
Expand All @@ -74,8 +74,66 @@ def test_loading_custom_templates(self):
"Template file did not contain our test string",
)

def test_multiple_custom_template_directories(self):
"""Tests that directories are searched in the right order if multiple custom
template directories are provided.
"""
# Create two temporary directories on the filesystem.
tempdirs = [
tempfile.TemporaryDirectory(),
tempfile.TemporaryDirectory(),
]

# Create one template in each directory, whose content is the index of the
# directory in the list.
template_filename = "my_template.html.j2"
for i in range(len(tempdirs)):
tempdir = tempdirs[i]
template_path = os.path.join(tempdir.name, template_filename)

with open(template_path, "w") as fp:
fp.write(str(i))
fp.flush()

# Retrieve the template.
template = (
self.hs.config.read_templates(
[template_filename],
(td.name for td in tempdirs),
)
)[0]

# Test that we got the template we dropped in the first directory in the list.
self.assertEqual(template.render(), "0")

# Add another template, this one only in the second directory in the list, so we
# can test that the second directory is still searched into when no matching file
# could be found in the first one.
other_template_name = "my_other_template.html.j2"
other_template_path = os.path.join(tempdirs[1].name, other_template_name)

with open(other_template_path, "w") as fp:
fp.write("hello world")
fp.flush()

# Retrieve the template.
template = (
self.hs.config.read_templates(
[other_template_name],
(td.name for td in tempdirs),
)
)[0]

# Test that the file has the expected content.
self.assertEqual(template.render(), "hello world")

# Cleanup the temporary directories manually since we're not using a context
# manager.
for td in tempdirs:
td.cleanup()

def test_loading_template_from_nonexistent_custom_directory(self):
with self.assertRaises(ConfigError):
self.hs.config.read_templates(
["some_filename.html"], "a_nonexistent_directory"
["some_filename.html"], ("a_nonexistent_directory",)
)