You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/07/13 15:46:01 UTC
(tooling-trusted-releases) 01/01: Use templates by default in the API when constructing vote messages
This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit e85ddb2611ea9c484e9bbb1776093fa937ac8e5a
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Mon Jul 13 16:33:12 2026 +0100
Use templates by default in the API when constructing vote messages
---
atr/api/__init__.py | 41 +++++++++++++--
atr/models/api.py | 6 ++-
tests/unit/test_api_vote_start.py | 105 ++++++++++++++++++++++++++++++++++++++
3 files changed, 146 insertions(+), 6 deletions(-)
diff --git a/atr/api/__init__.py b/atr/api/__init__.py
index 8bbf4cd6..2a4f31db 100644
--- a/atr/api/__init__.py
+++ b/atr/api/__init__.py
@@ -33,6 +33,7 @@ import atr.blueprints.api_auth as api_auth
import atr.cle as cle
import atr.config as config
import atr.constants as constants
+import atr.construct as construct
import atr.db as db
import atr.db.interaction as interaction
import atr.hashes as hashes
@@ -1975,16 +1976,17 @@ async def vote_start(
util.validate_email_recipients(data)
util.validate_vote_duration(data.vote_duration)
+ subject, body, fullname = await _vote_start_subject_and_body(data, release, asf_uid, scanned_revision)
+
async with storage.write_as_project_release_manager(data.project, asf_uid) as warm:
- # TODO: Get fullname and use instead of asf_uid
task = await warm.vote.start(
data.email_to,
data.project,
data.version,
data.vote_duration,
- data.subject,
- data.body,
- asf_uid,
+ subject,
+ body,
+ fullname,
permitted_recipients=permitted_recipients,
email_cc=data.email_cc,
email_bcc=data.email_bcc,
@@ -2275,6 +2277,37 @@ async def _resolve_signing_key_from_signature(
raise exceptions.NotFound("No matching signing key found for signature")
+async def _vote_start_subject_and_body(
+ data: models.api.VoteStartArgs, release: sql.Release, asf_uid: str, revision_number: safe.RevisionNumber
+) -> tuple[str, str, str]:
+ fullname = await _ldap_fullname(asf_uid)
+ if (data.subject is not None) and (data.body is not None):
+ return data.subject, data.body, fullname
+ subject_template = ""
+ body_template = ""
+ if data.subject is None:
+ subject_template = release.project.policy_start_vote_subject
+ if data.body is None:
+ if release.expedited:
+ body_template = construct.START_VOTE_EXPEDITED_DEFAULT
+ else:
+ body_template = release.project.policy_start_vote_template
+ options = construct.StartVoteOptions(
+ asfuid=asf_uid,
+ fullname=fullname,
+ project_key=data.project,
+ version_key=data.version,
+ revision_number=revision_number,
+ vote_duration=(0 if release.expedited else data.vote_duration),
+ )
+ subject, body = await construct.start_vote_subject_and_body(subject_template, body_template, options)
+ if data.subject is not None:
+ subject = data.subject
+ if data.body is not None:
+ body = data.body
+ return subject, body, fullname
+
+
async def _vote_tabulate_email_details(
release: sql.Release,
is_trusted: bool,
diff --git a/atr/models/api.py b/atr/models/api.py
index 439afd0a..62aeb7c4 100644
--- a/atr/models/api.py
+++ b/atr/models/api.py
@@ -900,8 +900,10 @@ class VoteStartArgs(schema.Strict):
email_cc: list[str] = schema.default_example([], ["dev@example.apache.org"])
email_bcc: list[str] = schema.default_example([], ["dev@example.apache.org"])
vote_duration: int = schema.example(72)
- subject: str = schema.example("[VOTE] Apache Example 0.0.1 release")
- body: str = schema.example("The Apache Example team is pleased to announce the release of Example 0.0.1...")
+ subject: str | None = schema.default_example(None, "[VOTE] Apache Example 0.0.1 release")
+ body: str | None = schema.default_example(
+ None, "The Apache Example team is pleased to announce the release of Example 0.0.1..."
+ )
second_round_email_to: str | None = schema.default_example(None, "general@incubator.apache.org")
notify_when_finished: bool = schema.default_example(False, False)
automatic_resolve_when_finished: bool = schema.default_example(False, False)
diff --git a/tests/unit/test_api_vote_start.py b/tests/unit/test_api_vote_start.py
new file mode 100644
index 00000000..e8875137
--- /dev/null
+++ b/tests/unit/test_api_vote_start.py
@@ -0,0 +1,105 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from types import SimpleNamespace
+
+import pytest
+
+import atr.api as api
+import atr.construct as construct
+import atr.models as models
+import atr.models.safe as safe
+
+
+@pytest.mark.asyncio
+async def test_vote_start_expedited_release_uses_expedited_template(monkeypatch) -> None:
+ calls = _patch_rendering(monkeypatch)
+
+ subject, body, fullname = await api._vote_start_subject_and_body(
+ _args(subject="Custom subject"), _release(expedited=True), "user", safe.RevisionNumber("00001")
+ )
+
+ assert (subject, body, fullname) == ("Custom subject", "RENDERED BODY", "Example User")
+ assert calls["subject"] == ""
+ assert calls["body"] == construct.START_VOTE_EXPEDITED_DEFAULT
+ assert calls["options"].vote_duration == 0
+
+
+@pytest.mark.asyncio
+async def test_vote_start_omitted_subject_and_body_render_policy_templates(monkeypatch) -> None:
+ calls = _patch_rendering(monkeypatch)
+
+ subject, body, fullname = await api._vote_start_subject_and_body(
+ _args(), _release(), "user", safe.RevisionNumber("00001")
+ )
+
+ assert (subject, body, fullname) == ("RENDERED SUBJECT", "RENDERED BODY", "Example User")
+ assert calls["subject"] == "[VOTE] {{PROJECT_NAME}} {{VERSION}}"
+ assert calls["body"] == "Body {{VERSION}}"
+ assert calls["options"].revision_number == safe.RevisionNumber("00001")
+ assert calls["options"].vote_duration == 72
+ assert calls["options"].fullname == "Example User"
+
+
+@pytest.mark.asyncio
+async def test_vote_start_supplied_subject_and_body_pass_through(monkeypatch) -> None:
+ calls = _patch_rendering(monkeypatch)
+
+ subject, body, fullname = await api._vote_start_subject_and_body(
+ _args(subject="Custom subject", body="Custom body"), _release(), "user", safe.RevisionNumber("00001")
+ )
+
+ assert (subject, body, fullname) == ("Custom subject", "Custom body", "Example User")
+ assert calls == {}
+
+
+def _args(subject: str | None = None, body: str | None = None) -> models.api.VoteStartArgs:
+ return models.api.VoteStartArgs(
+ project=safe.ProjectKey("example"),
+ version=safe.VersionKey("1.0.0"),
+ email_to="dev@example.apache.org",
+ vote_duration=72,
+ subject=subject,
+ body=body,
+ )
+
+
+def _patch_rendering(monkeypatch: pytest.MonkeyPatch) -> dict:
+ calls: dict = {}
+
+ async def fake_render(subject: str, body: str, options: construct.StartVoteOptions) -> tuple[str, str]:
+ calls["subject"] = subject
+ calls["body"] = body
+ calls["options"] = options
+ return "RENDERED SUBJECT", "RENDERED BODY"
+
+ async def fake_fullname(asf_uid: str) -> str:
+ return "Example User"
+
+ monkeypatch.setattr(api, "_ldap_fullname", fake_fullname)
+ monkeypatch.setattr(construct, "start_vote_subject_and_body", fake_render)
+ return calls
+
+
+def _release(expedited: bool = False) -> SimpleNamespace:
+ return SimpleNamespace(
+ expedited=expedited,
+ project=SimpleNamespace(
+ policy_start_vote_subject="[VOTE] {{PROJECT_NAME}} {{VERSION}}",
+ policy_start_vote_template="Body {{VERSION}}",
+ ),
+ )
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org