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 <[email protected]> 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: [email protected] For additional commands, e-mail: [email protected]
