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/10/05 15:25:23 UTC

(tooling-trusted-releases) branch sbp updated: Use clearer binding vote terminology for podling votes

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


The following commit(s) were added to refs/heads/sbp by this push:
     new 5a75a076 Use clearer binding vote terminology for podling votes
5a75a076 is described below

commit 5a75a076dfda78244d2523d3b2488955647a4aa3
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Mon Oct 5 16:24:42 2026 +0100

    Use clearer binding vote terminology for podling votes
---
 atr/get/vote.py                   | 46 +++++++++++++++++++++++++++++---------
 tests/unit/test_vote_rendering.py | 47 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 82 insertions(+), 11 deletions(-)

diff --git a/atr/get/vote.py b/atr/get/vote.py
index e20be2c6..daeb0bd1 100644
--- a/atr/get/vote.py
+++ b/atr/get/vote.py
@@ -372,10 +372,16 @@ def _is_podling_round_two(release: sql.Release) -> bool:
     return (committee is not None) and committee.is_podling and (release.podling_thread_id is not None)
 
 
-def _render_binding_status(page: htm.Block, is_binding: bool, binding_committee: str, vote_round: int | None) -> None:
-    binding_word, non_binding_word = user.binding_terminology(vote_round)
+async def _render_binding_status(
+    page: htm.Block, committee: sql.Committee, asf_uid: str, vote_round: int | None
+) -> str:
+    is_binding, binding_committee = await user.is_binding_for_release(committee, asf_uid, vote_round)
     committee_membership = _voting_committee_membership(binding_committee, vote_round)
     article = "an" if committee_membership[:1].lower() in {"a", "e", "i", "o", "u"} else "a"
+    if vote_round == 1:
+        is_ipmc_member, _incubator = await user.is_binding_for_release(committee, asf_uid, 2)
+        return _render_binding_status_round_one(page, f"{article} {committee_membership}", is_binding, is_ipmc_member)
+    binding_word, non_binding_word = user.binding_terminology(vote_round)
     if is_binding:
         page.p[
             f"As {article} {committee_membership}, your vote is ",
@@ -389,6 +395,31 @@ def _render_binding_status(page: htm.Block, is_binding: bool, binding_committee:
             htpy.strong[non_binding_word.lower()],
             " but is still valued by the community.",
         ]
+    return binding_word if is_binding else non_binding_word
+
+
+def _render_binding_status_round_one(
+    page: htm.Block, ppmc_membership: str, is_ppmc_member: bool, is_ipmc_member: bool
+) -> str:
+    if is_ppmc_member:
+        ppmc_status = [f"As {ppmc_membership}, your vote ", htpy.strong["counts"], " towards podling approval. "]
+    else:
+        ppmc_status = [
+            f"You are not {ppmc_membership}, so your vote ",
+            htpy.strong["does not count"],
+            " towards podling approval. ",
+        ]
+    if is_ipmc_member:
+        ipmc_status = ["As an Incubator PMC member, your vote is ", htpy.strong["binding"], " for Incubator approval."]
+    else:
+        ipmc_status = [
+            "You are not an Incubator PMC member, so your vote is ",
+            htpy.strong["non-binding"],
+            " for Incubator approval.",
+        ]
+    page.p[ppmc_status, ipmc_status]
+    labels = (["IPMC binding"] if is_ipmc_member else []) + (["PPMC vote"] if is_ppmc_member else [])
+    return ", ".join(labels) or "Non-binding"
 
 
 def _render_checklist_card(page: htm.Block, release: sql.Release) -> None:
@@ -650,10 +681,7 @@ async def _render_trusted_vote_authenticated(
         )
 
     vote_round = interaction.trusted_vote_round(release)
-    is_binding, binding_committee = await user.is_binding_for_release(release.committee, session.uid, vote_round)
-    binding_word, non_binding_word = user.binding_terminology(vote_round)
-    potency = binding_word if is_binding else non_binding_word
-    _render_binding_status(page, is_binding, binding_committee, vote_round)
+    potency = await _render_binding_status(page, release.committee, session.uid, vote_round)
 
     if latest_ballot is not None:
         is_carried = (latest_ballot.vote_round == 1) and (vote_round == 2)
@@ -712,11 +740,7 @@ async def _render_vote_authenticated(
         return
 
     vote_round = interaction.trusted_vote_round(release)
-    is_binding, binding_committee = await user.is_binding_for_release(release.committee, session.uid, vote_round)
-    binding_word, non_binding_word = user.binding_terminology(vote_round)
-
-    potency = binding_word if is_binding else non_binding_word
-    _render_binding_status(page, is_binding, binding_committee, vote_round)
+    potency = await _render_binding_status(page, release.committee, session.uid, vote_round)
     _render_vote_delivery(page, archive_url, release, vote_recipient, trusted_vote=False)
     vote_widget = _vote_decision_widget(potency)
     await _append_cast_vote_form(page, release, vote_widget)
diff --git a/tests/unit/test_vote_rendering.py b/tests/unit/test_vote_rendering.py
index 09e4684d..48c72d08 100644
--- a/tests/unit/test_vote_rendering.py
+++ b/tests/unit/test_vote_rendering.py
@@ -559,6 +559,53 @@ async def test_trusted_rendering_without_start_mid_disables_form_but_shows_lates
     latest_ballot_get.assert_awaited_once_with("project-1.0.0", 1, "voter")
 
 
+@pytest.mark.asyncio
+@pytest.mark.parametrize("vote_mode", [sql.VoteMode.EMAIL, sql.VoteMode.TRUSTED])
+@pytest.mark.parametrize(
+    ("is_podling", "podling_thread_id", "binding_by_round", "label"),
+    [
+        (True, None, {1: True, 2: True}, "IPMC binding, PPMC vote"),
+        (True, None, {1: False, 2: True}, "IPMC binding"),
+        (True, None, {1: True, 2: False}, "PPMC vote"),
+        (True, None, {1: False, 2: False}, "Non-binding"),
+        (True, "thread-abc", {2: True}, "Binding"),
+        (False, None, {None: False}, "Non-binding"),
+    ],
+)
+async def test_vote_decision_labels_reflect_membership(
+    render_app: quart.Quart,
+    monkeypatch: pytest.MonkeyPatch,
+    vote_mode: sql.VoteMode,
+    is_podling: bool,
+    podling_thread_id: str | None,
+    binding_by_round: dict[int | None, bool],
+    label: str,
+) -> None:
+    await _patch_render_dependencies(monkeypatch)
+    monkeypatch.setattr(
+        vote.user,
+        "is_binding_for_release",
+        mock.AsyncMock(side_effect=lambda _committee, _uid, vote_round: (binding_by_round[vote_round], "Project")),
+    )
+    monkeypatch.setattr(vote.interaction, "effective_latest_ballot_for_voter", mock.AsyncMock(return_value=None))
+    release = _release(vote_mode)
+    release.committee.is_podling = is_podling
+    release.podling_thread_id = podling_thread_id
+
+    async with render_app.test_request_context("/vote/project/1.0.0"):
+        page = htm.Block()
+        await vote._render_vote_authenticated(page, release, _session(), None, "dev@project.apache.org", None)
+
+    html = str(page.collect())
+    assert f"+1 ({label})" in html
+    assert f"-1 ({label})" in html
+    if is_podling and (podling_thread_id is None):
+        ppmc_word = "counts" if binding_by_round[1] else "does not count"
+        ipmc_word = "binding" if binding_by_round[2] else "non-binding"
+        assert f"your vote <strong>{ppmc_word}</strong> towards podling approval." in html
+        assert f"your vote is <strong>{ipmc_word}</strong> for Incubator approval." in html
+
+
 def test_vote_task_warnings_renders_delivery_warnings() -> None:
     task = _completed_vote_task()
     task.result = task.result.model_copy(


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org