You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ak...@apache.org on 2026/02/02 21:54:32 UTC
(tooling-trusted-releases) branch binding-vote-email-616 updated (cb6b976 -> 5960092)
This is an automated email from the ASF dual-hosted git repository.
akm pushed a change to branch binding-vote-email-616
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
discard cb6b976 Binding vote noted in email #616
add 0580960 Use a strict model for Quart cookie session data
new 5960092 Binding vote noted in email #616
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (cb6b976)
\
N -- N -- N refs/heads/binding-vote-email-616 (5960092)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
atr/admin/__init__.py | 18 +++++-
atr/get/test.py | 25 +++++----
atr/models/__init__.py | 4 +-
atr/{filters.py => models/session.py} | 19 +++++--
atr/server.py | 4 +-
atr/util.py | 100 ++++++++++++++++++++++++++++++++++
pyproject.toml | 3 +-
7 files changed, 150 insertions(+), 23 deletions(-)
copy atr/{filters.py => models/session.py} (65%)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org
(tooling-trusted-releases) 01/01: Binding vote noted in email #616
Posted by ak...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
akm pushed a commit to branch binding-vote-email-616
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 596009289ea8060375bc737b9e91815cdd933b74
Author: Andrew K. Musselman <ak...@apache.org>
AuthorDate: Mon Feb 2 12:30:30 2026 -0800
Binding vote noted in email #616
---
atr/post/vote.py | 20 ++++-
atr/storage/writers/vote.py | 44 ++++++++--
tests/unit/test_vote.py | 201 ++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 258 insertions(+), 7 deletions(-)
diff --git a/atr/post/vote.py b/atr/post/vote.py
index cdbb5ac..324a271 100644
--- a/atr/post/vote.py
+++ b/atr/post/vote.py
@@ -22,6 +22,7 @@ import atr.get as get
import atr.models.sql as sql
import atr.shared as shared
import atr.storage as storage
+import atr.user as user
import atr.web as web
@@ -40,8 +41,25 @@ async def selected_post(
vote = cast_vote_form.decision
comment = cast_vote_form.comment
+ # Determine if the vote is binding based on committee membership
+ # This logic mirrors atr/get/vote.py _render_vote_authenticated()
+ is_pmc_member = user.is_committee_member(release.committee, session.uid)
+
+ if release.committee.is_podling:
+ # For podlings, Incubator PMC membership grants binding status
+ async with storage.write() as write:
+ try:
+ _wacm = write.as_committee_member("incubator")
+ is_binding = True
+ except storage.AccessError:
+ is_binding = False
+ else:
+ is_binding = is_pmc_member
+
async with storage.write_as_committee_participant(release.committee.name) as wacm:
- email_recipient, error_message = await wacm.vote.send_user_vote(release, vote, comment, session.fullname)
+ email_recipient, error_message = await wacm.vote.send_user_vote(
+ release, vote, comment, session.fullname, is_binding
+ )
if error_message:
await quart.flash(error_message, "error")
diff --git a/atr/storage/writers/vote.py b/atr/storage/writers/vote.py
index 3084909..13ff310 100644
--- a/atr/storage/writers/vote.py
+++ b/atr/storage/writers/vote.py
@@ -81,6 +81,7 @@ class CommitteeParticipant(FoundationCommitter):
vote: str,
comment: str,
fullname: str,
+ is_binding: bool = False,
) -> tuple[str, str]:
# Get the email thread
latest_vote_task = await interaction.release_latest_vote_task(release)
@@ -102,12 +103,13 @@ class CommitteeParticipant(FoundationCommitter):
email_recipient = latest_vote_task.task_args["email_to"]
email_sender = f"{self.__asf_uid}@apache.org"
subject = f"Re: {original_subject}"
- body = [f"{vote.lower()} ({self.__asf_uid}) {fullname}"]
- if comment:
- body.append(f"{comment}")
- # Only include the signature if there is a comment
- body.append(f"-- \n{fullname} ({self.__asf_uid})")
- body_text = "\n\n".join(body)
+ body_text = format_vote_email_body(
+ vote=vote,
+ asf_uid=self.__asf_uid,
+ fullname=fullname,
+ is_binding=is_binding,
+ comment=comment,
+ )
in_reply_to = vote_thread_mid
task = sql.Task(
@@ -473,3 +475,33 @@ class CommitteeMember(CommitteeParticipant):
# def __committee_member_or_admin(self, committee: sql.Committee, asf_uid: str) -> None:
# if not (user.is_committee_member(committee, asf_uid) or user.is_admin(asf_uid)):
# raise storage.AccessError("You do not have permission to perform this action")
+
+
+def format_vote_email_body(
+ vote: str,
+ asf_uid: str,
+ fullname: str,
+ is_binding: bool,
+ comment: str = "",
+) -> str:
+ """Format the body of a vote email.
+
+ Args:
+ vote: The vote value (+1, 0, or -1)
+ asf_uid: The ASF user ID of the voter
+ fullname: The full name of the voter
+ is_binding: Whether this is a binding vote (PMC member)
+ comment: Optional comment to include
+
+ Returns:
+ The formatted email body text
+ """
+ if is_binding:
+ body = [f"{vote} (binding) ({asf_uid}) {fullname}"]
+ else:
+ body = [f"{vote} ({asf_uid}) {fullname}"]
+ if comment:
+ body.append(f"{comment}")
+ # Only include the signature if there is a comment
+ body.append(f"-- \n{fullname} ({asf_uid})")
+ return "\n\n".join(body)
diff --git a/tests/unit/test_vote.py b/tests/unit/test_vote.py
new file mode 100644
index 0000000..170bc49
--- /dev/null
+++ b/tests/unit/test_vote.py
@@ -0,0 +1,201 @@
+# 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.
+
+"""Tests for vote email formatting and binding vote determination."""
+
+import atr.storage.writers.vote as vote_writer
+import atr.user as user
+
+
+class _MockCommittee:
+ def __init__(
+ self,
+ name: str = "testproject",
+ display_name: str = "Test Project",
+ committee_members: list[str] | None = None,
+ committers: list[str] | None = None,
+ ):
+ self.name = name
+ self.display_name = display_name
+ self.committee_members = committee_members or []
+ self.committers = committers or []
+
+
+def test_binding_vote_body_format() -> None:
+ """Binding votes include '(binding)' in the email body."""
+ body = vote_writer.format_vote_email_body(
+ vote="+1",
+ asf_uid="pmcmember",
+ fullname="PMC Member",
+ is_binding=True,
+ )
+ assert body == "+1 (binding) (pmcmember) PMC Member"
+
+
+def test_non_binding_vote_body_format() -> None:
+ """Non-binding votes do not include '(binding)' in the email body."""
+ body = vote_writer.format_vote_email_body(
+ vote="+1",
+ asf_uid="contributor",
+ fullname="A Contributor",
+ is_binding=False,
+ )
+ assert body == "+1 (contributor) A Contributor"
+
+
+def test_negative_binding_vote_body_format() -> None:
+ """Negative binding votes are formatted correctly."""
+ body = vote_writer.format_vote_email_body(
+ vote="-1",
+ asf_uid="pmcmember",
+ fullname="PMC Member",
+ is_binding=True,
+ )
+ assert body == "-1 (binding) (pmcmember) PMC Member"
+
+
+def test_zero_binding_vote_body_format() -> None:
+ """Zero binding votes are formatted correctly."""
+ body = vote_writer.format_vote_email_body(
+ vote="0",
+ asf_uid="voter",
+ fullname="Abstaining Voter",
+ is_binding=True,
+ )
+ assert body == "0 (binding) (voter) Abstaining Voter"
+
+
+def test_zero_non_binding_vote_body_format() -> None:
+ """Zero non-binding votes are formatted correctly."""
+ body = vote_writer.format_vote_email_body(
+ vote="0",
+ asf_uid="voter",
+ fullname="Abstaining Voter",
+ is_binding=False,
+ )
+ assert body == "0 (voter) Abstaining Voter"
+
+
+def test_binding_vote_with_comment() -> None:
+ """Binding votes with comments include the comment and signature."""
+ body = vote_writer.format_vote_email_body(
+ vote="+1",
+ asf_uid="pmcmember",
+ fullname="PMC Member",
+ is_binding=True,
+ comment="Verified signatures and checksums. Tests pass.",
+ )
+ expected = (
+ "+1 (binding) (pmcmember) PMC Member\n\n"
+ "Verified signatures and checksums. Tests pass.\n\n"
+ "-- \nPMC Member (pmcmember)"
+ )
+ assert body == expected
+
+
+def test_non_binding_vote_with_comment() -> None:
+ """Non-binding votes with comments include the comment and signature."""
+ body = vote_writer.format_vote_email_body(
+ vote="+1",
+ asf_uid="contributor",
+ fullname="A Contributor",
+ is_binding=False,
+ comment="Looks good to me!",
+ )
+ expected = "+1 (contributor) A Contributor\n\nLooks good to me!\n\n-- \nA Contributor (contributor)"
+ assert body == expected
+
+
+def test_negative_vote_with_comment() -> None:
+ """Negative votes with comments are formatted correctly."""
+ body = vote_writer.format_vote_email_body(
+ vote="-1",
+ asf_uid="reviewer",
+ fullname="Careful Reviewer",
+ is_binding=True,
+ comment="Found a license issue in the dependencies.",
+ )
+ expected = (
+ "-1 (binding) (reviewer) Careful Reviewer\n\n"
+ "Found a license issue in the dependencies.\n\n"
+ "-- \nCareful Reviewer (reviewer)"
+ )
+ assert body == expected
+
+
+def test_empty_comment_no_signature() -> None:
+ """Empty comments do not add a signature."""
+ body = vote_writer.format_vote_email_body(
+ vote="+1",
+ asf_uid="pmcmember",
+ fullname="PMC Member",
+ is_binding=True,
+ comment="",
+ )
+ assert body == "+1 (binding) (pmcmember) PMC Member"
+ assert "-- \n" not in body
+
+
+def test_pmc_member_has_binding_vote() -> None:
+ """PMC members have binding votes."""
+ committee = _MockCommittee(
+ committee_members=["alice", "bob", "charlie"],
+ committers=["alice", "bob", "charlie", "dave", "eve"],
+ )
+ is_binding = user.is_committee_member(committee, "alice") # type: ignore[arg-type]
+ assert is_binding is True
+
+
+def test_committer_non_pmc_has_non_binding_vote() -> None:
+ """Committers who are not PMC members have non-binding votes."""
+ committee = _MockCommittee(
+ committee_members=["alice", "bob", "charlie"],
+ committers=["alice", "bob", "charlie", "dave", "eve"],
+ )
+ is_binding = user.is_committee_member(committee, "dave") # type: ignore[arg-type]
+ assert is_binding is False
+
+
+def test_non_committer_has_non_binding_vote() -> None:
+ """Non-committers have non-binding votes."""
+ committee = _MockCommittee(
+ committee_members=["alice", "bob", "charlie"],
+ committers=["alice", "bob", "charlie", "dave", "eve"],
+ )
+ is_binding = user.is_committee_member(committee, "frank") # type: ignore[arg-type]
+ assert is_binding is False
+
+
+def test_none_committee_returns_false() -> None:
+ """None committee returns False for membership."""
+ is_binding = user.is_committee_member(None, "anyone")
+ assert is_binding is False
+
+
+def test_admin_not_on_pmc_has_non_binding_vote() -> None:
+ """Admins who are not PMC members do not get binding votes."""
+ committee = _MockCommittee(
+ committee_members=["alice", "bob"],
+ committers=["alice", "bob", "charlie"],
+ )
+ # admin_user is not on this PMC
+ is_pmc_member = user.is_committee_member(committee, "admin_user") # type: ignore[arg-type]
+ assert is_pmc_member is False
+
+ # Binding is determined ONLY by PMC membership, not admin status
+ is_binding = is_pmc_member
+ assert is_binding is False
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org