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 2025/10/14 17:17:48 UTC

(tooling-trusted-releases) branch main updated: Allow everyone to view vote pages

This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new cb9a367  Allow everyone to view vote pages
cb9a367 is described below

commit cb9a367e36999979947829e5c822a34959c17f41
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Tue Oct 14 18:17:25 2025 +0100

    Allow everyone to view vote pages
---
 atr/routes/compose.py                             | 27 ++++++--
 atr/routes/vote.py                                | 81 ++++++++++++++---------
 atr/storage/readers/checks.py                     |  6 +-
 atr/storage/readers/releases.py                   |  6 +-
 atr/storage/readers/tokens.py                     |  1 +
 atr/templates/check-selected-candidate-forms.html |  2 +-
 atr/templates/check-selected-release-info.html    |  4 +-
 7 files changed, 76 insertions(+), 51 deletions(-)

diff --git a/atr/routes/compose.py b/atr/routes/compose.py
index 9e91c20..88803b3 100644
--- a/atr/routes/compose.py
+++ b/atr/routes/compose.py
@@ -38,13 +38,15 @@ if TYPE_CHECKING:
 
 
 async def check(
-    session: route.CommitterSession,
+    session: route.CommitterSession | None,
     release: sql.Release,
     task_mid: str | None = None,
     form: wtforms.Form | None = None,
     hidden_form: wtforms.Form | None = None,
     archive_url: str | None = None,
     vote_task: sql.Task | None = None,
+    can_vote: bool = False,
+    can_resolve: bool = False,
 ) -> response.Response | str:
     base_path = util.release_directory(release)
 
@@ -53,13 +55,22 @@ async def check(
     paths = [path async for path in util.paths_recursive(base_path)]
     paths.sort()
 
-    async with storage.read() as read:
+    asf_uid = session.uid if (session is not None) else None
+    async with storage.read(asf_uid) as read:
         ragp = read.as_general_public()
         info = await ragp.releases.path_info(release, paths)
 
     user_ssh_keys: Sequence[sql.SSHKey] = []
-    async with db.session() as data:
-        user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all()
+    asf_id: str | None = None
+    server_domain: str | None = None
+    server_host: str | None = None
+
+    if session is not None:
+        asf_id = session.uid
+        server_domain = session.app_host.split(":", 1)[0]
+        server_host = session.app_host
+        async with db.session() as data:
+            user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all()
 
     # Get the number of ongoing tasks for the current revision
     ongoing_tasks_count = 0
@@ -100,9 +111,9 @@ async def check(
         ongoing_tasks_count=ongoing_tasks_count,
         delete_form=delete_draft_form,
         delete_file_form=delete_file_form,
-        asf_id=session.uid,
-        server_domain=session.app_host.split(":", 1)[0],
-        server_host=session.app_host,
+        asf_id=asf_id,
+        server_domain=server_domain,
+        server_host=server_host,
         user_ssh_keys=user_ssh_keys,
         format_datetime=util.format_datetime,
         models=sql,
@@ -115,6 +126,8 @@ async def check(
         hidden_form=hidden_form,
         has_files=has_files,
         strict_checking_errors=strict_checking_errors,
+        can_vote=can_vote,
+        can_resolve=can_resolve,
     )
 
 
diff --git a/atr/routes/vote.py b/atr/routes/vote.py
index 634ecda..d4999b2 100644
--- a/atr/routes/vote.py
+++ b/atr/routes/vote.py
@@ -29,6 +29,7 @@ import atr.route as route
 import atr.routes.compose as compose
 import atr.routes.mapping as mapping
 import atr.storage as storage
+import atr.user as user
 import atr.util as util
 
 
@@ -40,11 +41,11 @@ class CastVoteForm(forms.Typed):
     submit = forms.submit("Submit vote")
 
 
-@route.committer("/vote/<project_name>/<version_name>")
-async def selected(session: route.CommitterSession, project_name: str, version_name: str) -> response.Response | str:
+@route.public("/vote/<project_name>/<version_name>")
+async def selected(
+    session: route.CommitterSession | None, project_name: str, version_name: str
+) -> response.Response | str:
     """Show the contents of the release candidate draft."""
-    await session.check_access(project_name)
-
     async with db.session() as data:
         release = await data.release(
             project_name=project_name,
@@ -52,8 +53,22 @@ async def selected(session: route.CommitterSession, project_name: str, version_n
             _committee=True,
             _project_release_policy=True,
         ).demand(base.ASFQuartException("Release does not exist", errorcode=404))
+
     if release.phase != sql.ReleasePhase.RELEASE_CANDIDATE:
+        if session is None:
+            raise base.ASFQuartException("Release is not a candidate", errorcode=404)
         return await mapping.release_as_redirect(session, release)
+
+    if release.committee is None:
+        raise ValueError("Release has no committee")
+
+    is_authenticated = session is not None
+    is_committee_member = is_authenticated and (
+        user.is_committee_member(release.committee, session.uid) or user.is_admin(session.uid)
+    )
+    can_vote = is_committee_member
+    can_resolve = is_committee_member
+
     latest_vote_task = await interaction.release_latest_vote_task(release)
     archive_url = None
     task_mid = None
@@ -74,37 +89,38 @@ async def selected(session: route.CommitterSession, project_name: str, version_n
 
         # Move task_mid_get here?
         task_mid = interaction.task_mid_get(latest_vote_task)
-        async with storage.write() as write:
+        asf_uid = session.uid if (session is not None) else None
+        async with storage.write(asf_uid) as write:
             wagp = write.as_general_public()
             archive_url = await wagp.cache.get_message_archive_url(task_mid)
 
-    # Special form for the [ Resolve vote ] button, to make it POST
-    hidden_form = await forms.Hidden.create_form()
-    hidden_form.hidden_field.data = archive_url or ""
-    hidden_form.submit.label.text = "Resolve vote"
-
-    if release.committee is None:
-        raise ValueError("Release has no committee")
+    hidden_form = None
+    if can_resolve:
+        # Special form for the [ Resolve vote ] button, to make it POST
+        hidden_form = await forms.Hidden.create_form()
+        hidden_form.hidden_field.data = archive_url or ""
+        hidden_form.submit.label.text = "Resolve vote"
 
-    # Form to cast a vote
-    form = await CastVoteForm.create_form()
-    async with storage.write() as write:
-        try:
-            if release.committee.is_podling:
-                _wacm = write.as_committee_member("incubator")
-            else:
-                _wacm = write.as_committee_member(release.committee.name)
-            potency = "Binding"
-        except storage.AccessError:
-            potency = "Non-binding"
-    forms.choices(
-        form.vote_value,
-        choices=[
-            ("+1", f"+1 ({potency})"),
-            ("0", "0"),
-            ("-1", f"-1 ({potency})"),
-        ],
-    )
+    form = None
+    if can_vote:
+        form = await CastVoteForm.create_form()
+        async with storage.write() as write:
+            try:
+                if release.committee.is_podling:
+                    _wacm = write.as_committee_member("incubator")
+                else:
+                    _wacm = write.as_committee_member(release.committee.name)
+                potency = "Binding"
+            except storage.AccessError:
+                potency = "Non-binding"
+        forms.choices(
+            form.vote_value,
+            choices=[
+                ("+1", f"+1 ({potency})"),
+                ("0", "0"),
+                ("-1", f"-1 ({potency})"),
+            ],
+        )
 
     return await compose.check(
         session,
@@ -114,6 +130,8 @@ async def selected(session: route.CommitterSession, project_name: str, version_n
         hidden_form=hidden_form,
         archive_url=archive_url,
         vote_task=latest_vote_task,
+        can_vote=can_vote,
+        can_resolve=can_resolve,
     )
 
 
@@ -137,6 +155,7 @@ async def selected_post(session: route.CommitterSession, project_name: str, vers
                 _wacm = write.as_committee_member(release.committee.name)
             potency = "Binding"
         except storage.AccessError:
+            # Participant, due to session.check_access above
             potency = "Non-binding"
 
     form = await CastVoteForm.create_form(data=await quart.request.form)
diff --git a/atr/storage/readers/checks.py b/atr/storage/readers/checks.py
index 079a2f5..8cf44b1 100644
--- a/atr/storage/readers/checks.py
+++ b/atr/storage/readers/checks.py
@@ -42,11 +42,7 @@ class GeneralPublic:
         self.__read = read
         self.__read_as = read_as
         self.__data = data
-        if asf_uid is None:
-            asf_uid = read.authorisation.asf_uid
-        if asf_uid is None:
-            raise ValueError("An ASF UID is required")
-        self.__asf_uid = asf_uid
+        self.__asf_uid = read.authorisation.asf_uid
 
     async def by_release_path(self, release: sql.Release, rel_path: pathlib.Path) -> types.CheckResults:
         if release.committee is None:
diff --git a/atr/storage/readers/releases.py b/atr/storage/readers/releases.py
index 193ab51..b1ecf6a 100644
--- a/atr/storage/readers/releases.py
+++ b/atr/storage/readers/releases.py
@@ -39,11 +39,7 @@ class GeneralPublic:
         self.__read = read
         self.__read_as = read_as
         self.__data = data
-        if asf_uid is None:
-            asf_uid = read.authorisation.asf_uid
-        if asf_uid is None:
-            raise ValueError("An ASF UID is required")
-        self.__asf_uid = asf_uid
+        self.__asf_uid = read.authorisation.asf_uid
 
     async def path_info(self, release: sql.Release, paths: list[pathlib.Path]) -> types.PathInfo | None:
         info = types.PathInfo()
diff --git a/atr/storage/readers/tokens.py b/atr/storage/readers/tokens.py
index 5bff63d..d892042 100644
--- a/atr/storage/readers/tokens.py
+++ b/atr/storage/readers/tokens.py
@@ -30,6 +30,7 @@ class GeneralPublic:
         self.__read = read
         self.__read_as = read_as
         self.__data = data
+        self.__asf_uid = read.authorisation.asf_uid
 
 
 class FoundationCommitter(GeneralPublic):
diff --git a/atr/templates/check-selected-candidate-forms.html b/atr/templates/check-selected-candidate-forms.html
index 17ca5fd..8cc096e 100644
--- a/atr/templates/check-selected-candidate-forms.html
+++ b/atr/templates/check-selected-candidate-forms.html
@@ -3,7 +3,7 @@
 
 {% endif %}
 
-{% if form %}
+{% if can_vote and form %}
   <h2>Cast your vote</h2>
 
   <form method="post"
diff --git a/atr/templates/check-selected-release-info.html b/atr/templates/check-selected-release-info.html
index 9b5fdc7..869fa22 100644
--- a/atr/templates/check-selected-release-info.html
+++ b/atr/templates/check-selected-release-info.html
@@ -77,10 +77,10 @@
            class="btn btn-primary"><i class="bi bi-download me-1"></i> Download files</a>
         <a href="{{ as_url(routes.candidate.view, project_name=release.project.name, version_name=release.version) }}"
            class="btn btn-secondary"><i class="bi bi-eye me-1"></i> View files</a>
-        {% if release.vote_manual %}
+        {% if can_resolve and release.vote_manual %}
           <a href="{{ as_url(routes.resolve.manual_selected, project_name=release.project.name, version_name=release.version) }}"
              class="btn btn-success"><i class="bi bi-clipboard-check me-1"></i> Resolve vote</a>
-        {% elif hidden_form %}
+        {% elif can_resolve and hidden_form %}
           <form action="{{ as_url(routes.resolve.tabulated_selected_post, project_name=release.project.name, version_name=release.version) }}"
                 method="post"
                 class="mb-0">


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