diff --git a/package/share/ahriman/settings/ahriman.ini b/package/share/ahriman/settings/ahriman.ini index 440660eb..56b31ef9 100644 --- a/package/share/ahriman/settings/ahriman.ini +++ b/package/share/ahriman/settings/ahriman.ini @@ -54,14 +54,17 @@ use_utf = yes [email] no_empty_report = yes -template_path = /usr/share/ahriman/templates/email-index.jinja2 +template = email-index.jinja2 +templates = /usr/share/ahriman/templates ssl = disabled [html] -template_path = /usr/share/ahriman/templates/repo-index.jinja2 +template = repo-index.jinja2 +templates = /usr/share/ahriman/templates [telegram] -template_path = /usr/share/ahriman/templates/telegram-index.jinja2 +template = telegram-index.jinja2 +templates = /usr/share/ahriman/templates [upload] target = diff --git a/src/ahriman/core/configuration/configuration.py b/src/ahriman/core/configuration/configuration.py index 4089cb5b..07a52f53 100644 --- a/src/ahriman/core/configuration/configuration.py +++ b/src/ahriman/core/configuration/configuration.py @@ -82,6 +82,7 @@ class Configuration(configparser.RawConfigParser): converters={ "list": shlex.split, "path": self._convert_path, + "pathlist": lambda value: [self._convert_path(element) for element in shlex.split(value)], } ) @@ -241,6 +242,8 @@ class Configuration(configparser.RawConfigParser): def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore[empty-body] + def getpathlist(self, *args: Any, **kwargs: Any) -> list[Path]: ... # type: ignore[empty-body] + def gettype(self, section: str, repository_id: RepositoryId, *, fallback: str | None = None) -> tuple[str, str]: """ get type variable with fallback to old logic. Despite the fact that it has same semantics as other get* methods, diff --git a/src/ahriman/core/configuration/schema.py b/src/ahriman/core/configuration/schema.py index 5b10dee7..818ee9a0 100644 --- a/src/ahriman/core/configuration/schema.py +++ b/src/ahriman/core/configuration/schema.py @@ -264,10 +264,14 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = { "path_exists": True, }, "templates": { - "type": "path", - "coerce": "absolute_path", - "required": True, - "path_exists": True, + "type": "list", + "coerce": "list", + "schema": { + "type": "path", + "coerce": "absolute_path", + "path_exists": True, + }, + "empty": False, }, "timeout": { "type": "integer", diff --git a/src/ahriman/core/report/email.py b/src/ahriman/core/report/email.py index ad5360a9..e074ae32 100644 --- a/src/ahriman/core/report/email.py +++ b/src/ahriman/core/report/email.py @@ -37,7 +37,6 @@ class Email(Report, JinjaTemplate): email report generator Attributes: - full_template_path(Path): path to template for full package list host(str): SMTP host to connect no_empty_report(bool): skip empty report generation password(str | None): password to authenticate via SMTP @@ -45,7 +44,8 @@ class Email(Report, JinjaTemplate): receivers(list[str]): list of receivers emails sender(str): sender email address ssl(SmtpSSLSettings): SSL mode for SMTP connection - template_path(Path): path to template for built packages + template(Path | str): path or name to template for built packages + template_full(Path | str | None): path or name to template for full package list user(str | None): username to authenticate via SMTP """ @@ -61,8 +61,10 @@ class Email(Report, JinjaTemplate): Report.__init__(self, repository_id, configuration) JinjaTemplate.__init__(self, repository_id, configuration, section) - self.full_template_path = configuration.getpath(section, "full_template_path", fallback=None) - self.template_path = configuration.getpath(section, "template_path") + self.template = configuration.get(section, "template", fallback=None) or \ + configuration.getpath(section, "template_path") + self.template_full = configuration.get(section, "template_full", fallback=None) or \ + configuration.getpath(section, "full_template_path", fallback=None) # base smtp settings self.host = configuration.get(section, "host") @@ -114,9 +116,10 @@ class Email(Report, JinjaTemplate): """ if self.no_empty_report and not result.success: return - text = self.make_html(result, self.template_path) - if self.full_template_path is not None: - attachments = {"index.html": self.make_html(Result(success=packages), self.full_template_path)} - else: - attachments = {} + + text = self.make_html(result, self.template) + attachments = {} + if self.template_full is not None: + attachments = {"index.html": self.make_html(Result(success=packages), self.template_full)} + self._send(text, attachments) diff --git a/src/ahriman/core/report/html.py b/src/ahriman/core/report/html.py index 11d89a87..55deb739 100644 --- a/src/ahriman/core/report/html.py +++ b/src/ahriman/core/report/html.py @@ -31,7 +31,7 @@ class HTML(Report, JinjaTemplate): Attributes: report_path(Path): output path to html report - template_path(Path): path to template for full package list + template(Path | str): name or path to template for full package list """ def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: @@ -47,7 +47,8 @@ class HTML(Report, JinjaTemplate): JinjaTemplate.__init__(self, repository_id, configuration, section) self.report_path = configuration.getpath(section, "path") - self.template_path = configuration.getpath(section, "template_path") + self.template = configuration.get(section, "template", fallback=None) or \ + configuration.getpath(section, "template_path") def generate(self, packages: list[Package], result: Result) -> None: """ @@ -57,5 +58,5 @@ class HTML(Report, JinjaTemplate): packages(list[Package]): list of packages to generate report result(Result): build result """ - html = self.make_html(Result(success=packages), self.template_path) + html = self.make_html(Result(success=packages), self.template) self.report_path.write_text(html, encoding="utf8") diff --git a/src/ahriman/core/report/jinja_template.py b/src/ahriman/core/report/jinja_template.py index 4826d29c..1ed7607d 100644 --- a/src/ahriman/core/report/jinja_template.py +++ b/src/ahriman/core/report/jinja_template.py @@ -57,11 +57,12 @@ class JinjaTemplate: * repository - repository name, string, required Attributes: + default_pgp_key(str | None): default PGP key homepage(str | None): homepage link if any (for footer) link_path(str): prefix fo packages to download name(str): repository name - default_pgp_key(str | None): default PGP key sign_targets(set[SignSettings]): targets to sign enabled in configuration + templates(list[Path]): list of directories with templates """ def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: @@ -73,6 +74,8 @@ class JinjaTemplate: configuration(Configuration): configuration instance section(str): settings section name """ + self.templates = configuration.getpathlist(section, "templates", fallback=[]) + self.link_path = configuration.get(section, "link_path") # base template vars @@ -81,18 +84,23 @@ class JinjaTemplate: self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration) - def make_html(self, result: Result, template_path: Path) -> str: + def make_html(self, result: Result, template_name: Path | str) -> str: """ generate report for the specified packages Args: result(Result): build result - template_path(Path): path to jinja template + template_name(Path | str): name of the template or path to it (legacy configuration) """ + templates = self.templates[:] + if isinstance(template_name, Path): + templates.append(template_name.parent) + template_name = template_name.name + # idea comes from https://stackoverflow.com/a/38642558 - loader = jinja2.FileSystemLoader(searchpath=template_path.parent) + loader = jinja2.FileSystemLoader(searchpath=templates) environment = jinja2.Environment(loader=loader, autoescape=True) - template = environment.get_template(template_path.name) + template = environment.get_template(template_name) content = [ { diff --git a/src/ahriman/core/report/report_trigger.py b/src/ahriman/core/report/report_trigger.py index 19ac542d..f6c32ae7 100644 --- a/src/ahriman/core/report/report_trigger.py +++ b/src/ahriman/core/report/report_trigger.py @@ -67,6 +67,8 @@ class ReportTrigger(Trigger): "full_template_path": { "type": "path", "coerce": "absolute_path", + "excludes": ["template_full"], + "required": True, "path_exists": True, }, "homepage": { @@ -111,12 +113,36 @@ class ReportTrigger(Trigger): "type": "string", "allowed": ["ssl", "starttls", "disabled"], }, + "template": { + "type": "string", + "excludes": ["template_path"], + "dependencies": ["templates"], + "required": True, + "empty": False, + }, + "template_full": { + "type": "string", + "excludes": ["template_path"], + "dependencies": ["templates"], + "required": True, + "empty": False, + }, "template_path": { "type": "path", "coerce": "absolute_path", + "excludes": ["template"], "required": True, "path_exists": True, }, + "templates": { + "type": "list", + "coerce": "list", + "schema": { + "type": "path", + "coerce": "absolute_path", + "path_exists": True, + }, + }, "user": { "type": "string", }, @@ -143,12 +169,29 @@ class ReportTrigger(Trigger): "coerce": "absolute_path", "required": True, }, + "template": { + "type": "string", + "excludes": ["template_path"], + "dependencies": ["templates"], + "required": True, + "empty": False, + }, "template_path": { "type": "path", "coerce": "absolute_path", + "excludes": ["template"], "required": True, "path_exists": True, }, + "templates": { + "type": "list", + "coerce": "list", + "schema": { + "type": "path", + "coerce": "absolute_path", + "path_exists": True, + }, + }, }, }, "telegram": { @@ -175,9 +218,17 @@ class ReportTrigger(Trigger): "required": True, "is_url": [], }, + "template": { + "type": "string", + "excludes": ["template_path"], + "dependencies": ["templates"], + "required": True, + "empty": False, + }, "template_path": { "type": "path", "coerce": "absolute_path", + "excludes": ["template"], "required": True, "path_exists": True, }, @@ -185,6 +236,15 @@ class ReportTrigger(Trigger): "type": "string", "allowed": ["MarkdownV2", "HTML", "Markdown"], }, + "templates": { + "type": "list", + "coerce": "list", + "schema": { + "type": "path", + "coerce": "absolute_path", + "path_exists": True, + }, + }, "timeout": { "type": "integer", "coerce": "integer", diff --git a/src/ahriman/core/report/telegram.py b/src/ahriman/core/report/telegram.py index 46145150..4780b289 100644 --- a/src/ahriman/core/report/telegram.py +++ b/src/ahriman/core/report/telegram.py @@ -35,7 +35,7 @@ class Telegram(Report, JinjaTemplate, SyncHttpClient): TELEGRAM_MAX_CONTENT_LENGTH(int): (class attribute) max content length of the message api_key(str): bot api key chat_id(str): chat id to post message, either string with @ or integer - template_path(Path): path to template for built packages + template(Path | str): name or path to template for built packages template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown """ @@ -57,7 +57,8 @@ class Telegram(Report, JinjaTemplate, SyncHttpClient): self.api_key = configuration.get(section, "api_key") self.chat_id = configuration.get(section, "chat_id") - self.template_path = configuration.getpath(section, "template_path") + self.template = configuration.get(section, "template", fallback=None) or \ + configuration.getpath(section, "template_path") self.template_type = configuration.get(section, "template_type", fallback="HTML") def _send(self, text: str) -> None: @@ -83,7 +84,7 @@ class Telegram(Report, JinjaTemplate, SyncHttpClient): """ if not result.success: return - text = self.make_html(result, self.template_path) + text = self.make_html(result, self.template) # telegram content is limited by 4096 symbols, so we are going to split the message by new lines # to fit into this restriction while len(text) > self.TELEGRAM_MAX_CONTENT_LENGTH: diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 25d937a6..4fe7c823 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -147,7 +147,8 @@ def setup_service(repository_id: RepositoryId, configuration: Configuration, spa setup_cors(application) application.logger.info("setup templates") - aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(configuration.getpath("web", "templates"))) + templates = configuration.getpathlist("web", "templates") + aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(searchpath=templates)) application.logger.info("setup configuration") application["configuration"] = configuration diff --git a/tests/ahriman/core/configuration/test_configuration.py b/tests/ahriman/core/configuration/test_configuration.py index db635f46..d5b207cf 100644 --- a/tests/ahriman/core/configuration/test_configuration.py +++ b/tests/ahriman/core/configuration/test_configuration.py @@ -229,6 +229,19 @@ def test_getpath_without_fallback(configuration: Configuration) -> None: assert configuration.getpath("build", "option") +def test_getpathlist(configuration: Configuration) -> None: + """ + must extract path list + """ + path = Path("/a/b/c") + configuration.set_option("build", "path", f"""{path} {path.relative_to("/")}""") + + result = configuration.getpathlist("build", "path") + assert all(element.is_absolute() for element in result) + assert path in result + assert all(element.is_relative_to("/") for element in result) + + def test_gettype(configuration: Configuration) -> None: """ must extract type from variable diff --git a/tests/ahriman/core/report/test_email.py b/tests/ahriman/core/report/test_email.py index b835df7f..2515d857 100644 --- a/tests/ahriman/core/report/test_email.py +++ b/tests/ahriman/core/report/test_email.py @@ -8,6 +8,35 @@ from ahriman.models.package import Package from ahriman.models.result import Result +def test_template(configuration: Configuration) -> None: + """ + must correctly parse template name and path + """ + template = configuration.get("email", "template") + root, repository_id = configuration.check_loaded() + + assert Email(repository_id, configuration, "email").template == template + + configuration.remove_option("email", "template") + configuration.set_option("email", "template_path", template) + assert Email(repository_id, configuration, "email").template == root.parent / template + + +def test_template_full(configuration: Configuration) -> None: + """ + must correctly parse template name and path + """ + template = "template" + root, repository_id = configuration.check_loaded() + + configuration.set_option("email", "template_full", template) + assert Email(repository_id, configuration, "email").template_full == template + + configuration.remove_option("email", "template_full") + configuration.set_option("email", "full_template_path", template) + assert Email(repository_id, configuration, "email").template_full == root.parent / template + + def test_send(email: Email, mocker: MockerFixture) -> None: """ must send an email with attachment @@ -115,7 +144,7 @@ def test_generate_with_built_and_full_path(email: Email, package_ahriman: Packag """ send_mock = mocker.patch("ahriman.core.report.email.Email._send") - email.full_template_path = email.template_path + email.template_full = email.template email.generate([package_ahriman], result) send_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int)) diff --git a/tests/ahriman/core/report/test_html.py b/tests/ahriman/core/report/test_html.py index 71690118..2a3f9611 100644 --- a/tests/ahriman/core/report/test_html.py +++ b/tests/ahriman/core/report/test_html.py @@ -8,6 +8,20 @@ from ahriman.models.package import Package from ahriman.models.result import Result +def test_template(configuration: Configuration) -> None: + """ + must correctly parse template name and path + """ + template = configuration.get("html", "template") + root, repository_id = configuration.check_loaded() + + assert HTML(repository_id, configuration, "html").template == template + + configuration.remove_option("html", "template") + configuration.set_option("html", "template_path", template) + assert HTML(repository_id, configuration, "html").template == root.parent / template + + def test_generate(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None: """ must generate report diff --git a/tests/ahriman/core/report/test_jinja_template.py b/tests/ahriman/core/report/test_jinja_template.py index dd735d77..65cc5c23 100644 --- a/tests/ahriman/core/report/test_jinja_template.py +++ b/tests/ahriman/core/report/test_jinja_template.py @@ -8,7 +8,17 @@ def test_generate(configuration: Configuration, package_ahriman: Package) -> Non """ must generate html report """ - path = configuration.getpath("html", "template_path") + name = configuration.getpath("html", "template") + _, repository_id = configuration.check_loaded() + report = JinjaTemplate(repository_id, configuration, "html") + assert report.make_html(Result(success=[package_ahriman]), name) + + +def test_generate_from_path(configuration: Configuration, package_ahriman: Package) -> None: + """ + must generate html report from path + """ + path = configuration.getpath("html", "templates") / configuration.get("html", "template") _, repository_id = configuration.check_loaded() report = JinjaTemplate(repository_id, configuration, "html") assert report.make_html(Result(success=[package_ahriman]), path) diff --git a/tests/ahriman/core/report/test_telegram.py b/tests/ahriman/core/report/test_telegram.py index 047824fe..76cadbba 100644 --- a/tests/ahriman/core/report/test_telegram.py +++ b/tests/ahriman/core/report/test_telegram.py @@ -10,6 +10,20 @@ from ahriman.models.package import Package from ahriman.models.result import Result +def test_template(configuration: Configuration) -> None: + """ + must correctly parse template name and path + """ + template = configuration.get("telegram", "template") + root, repository_id = configuration.check_loaded() + + assert Telegram(repository_id, configuration, "telegram").template == template + + configuration.remove_option("telegram", "template") + configuration.set_option("telegram", "template_path", template) + assert Telegram(repository_id, configuration, "telegram").template == root.parent / template + + def test_send(telegram: Telegram, mocker: MockerFixture) -> None: """ must send a message diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index d12b4c3b..fc9ed8c7 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -63,7 +63,8 @@ no_empty_report = no port = 587 receivers = mail@example.com sender = mail@example.com -template_path = ../web/templates/repo-index.jinja2 +template = repo-index.jinja2 +templates = ../web/templates [console] use_utf = yes @@ -72,7 +73,8 @@ use_utf = yes path = homepage = link_path = -template_path = ../web/templates/repo-index.jinja2 +template = repo-index.jinja2 +templates = ../web/templates [remote-call] manual = yes @@ -82,7 +84,8 @@ api_key = apikey chat_id = @ahrimantestchat homepage = link_path = -template_path = ../web/templates/telegram-index.jinja2 +template = telegram-index.jinja2 +templates = ../web/templates [upload] target =