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-release.git


The following commit(s) were added to refs/heads/main by this push:
     new c619eff  Show an error instead of the form to start a vote if there 
are no files
c619eff is described below

commit c619eff44e576996057c64ef75d30d689f6bc45f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Jun 18 20:07:54 2025 +0100

    Show an error instead of the form to start a vote if there are no files
---
 atr/routes/voting.py | 155 ++++++++++++++++++++++++++++++++-------------------
 atr/util.py          |  13 +++++
 2 files changed, 110 insertions(+), 58 deletions(-)

diff --git a/atr/routes/voting.py b/atr/routes/voting.py
index f366b94..a98ed40 100644
--- a/atr/routes/voting.py
+++ b/atr/routes/voting.py
@@ -123,71 +123,32 @@ async def selected_revision(
             vote_duration_choice: int = util.unwrap(form.vote_duration.data)
             subject_data: str = util.unwrap(form.subject.data)
             body_data: str = util.unwrap(form.body.data)
-
-            if committee is None:
-                raise base.ASFQuartException("Release has no associated 
committee", errorcode=400)
-
-            if email_to not in permitted_recipients:
-                # This will be checked again by tasks/vote.py for extra safety
-                raise base.ASFQuartException("Invalid mailing list choice", 
errorcode=400)
-
-            # Check for ongoing tasks
-            ongoing_tasks = await interaction.tasks_ongoing(project_name, 
version_name, selected_revision_number)
-            if ongoing_tasks > 0:
-                return await session.redirect(
-                    selected_revision,
-                    project_name=project_name,
-                    version_name=version_name,
-                    revision=selected_revision_number,
-                    error="All checks must be completed before starting a 
vote.",
-                )
-
-            # This sets the phase to RELEASE_CANDIDATE
-            error = await _promote(data, release.name, 
selected_revision_number)
-            if error:
-                return await session.redirect(root.index, error=error)
-
-            # Store when the release was put into the voting phase
-            release.vote_started = datetime.datetime.now(datetime.UTC)
-
-            # TODO: We also need to store the duration of the vote
-            # We can't allow resolution of the vote until the duration has 
elapsed
-            # But we allow the user to specify in the form
-            # And yet we also have ReleasePolicy.min_hours
-            # Presumably this sets the default, and the form takes precedence?
-            # ReleasePolicy.min_hours can also be 0, though
-
-            # Create a task for vote initiation
-            task = models.Task(
-                status=models.TaskStatus.QUEUED,
-                task_type=models.TaskType.VOTE_INITIATE,
-                task_args=tasks_vote.Initiate(
-                    release_name=release.name,
-                    email_to=email_to,
-                    vote_duration=vote_duration_choice,
-                    initiator_id=session.uid,
-                    initiator_fullname=session.fullname,
-                    subject=subject_data,
-                    body=body_data,
-                ).model_dump(),
-                project_name=project_name,
-                version_name=version,
+            return await _start_vote(
+                committee,
+                email_to,
+                permitted_recipients,
+                project_name,
+                version_name,
+                selected_revision_number,
+                session,
+                vote_duration_choice,
+                subject_data,
+                body_data,
+                data,
+                release,
+                version,
             )
-            data.add(task)
-            await data.commit()
 
-            # TODO: We should log all outgoing email and the session so that 
users can confirm
-            # And can be warned if there was a failure
-            # (The message should be shown on the vote resolution page)
+        keys_warning = await _keys_warning(release)
+        has_files = await util.has_files(release)
+        if not has_files:
             return await session.redirect(
-                vote.selected,
-                success=f"The vote announcement email will soon be sent to 
{email_to}.",
+                compose.selected,
+                error="This release candidate draft has no files yet. Please 
add some files before starting a vote.",
                 project_name=project_name,
                 version_name=version,
             )
 
-        keys_warning = await _keys_warning(release)
-
         # For GET requests or failed POST validation
         return await template.render(
             "voting-selected-revision.html",
@@ -253,3 +214,81 @@ async def _promote(
         return "A newer revision appeared, please refresh and try again."
     await data.commit()
     return None
+
+
+async def _start_vote(
+    committee: models.Committee,
+    email_to: str,
+    permitted_recipients: list[str],
+    project_name: str,
+    version_name: str,
+    selected_revision_number: str,
+    session: routes.CommitterSession,
+    vote_duration_choice: int,
+    subject_data: str,
+    body_data: str,
+    data: db.Session,
+    release: models.Release,
+    version: str,
+):
+    if committee is None:
+        raise base.ASFQuartException("Release has no associated committee", 
errorcode=400)
+
+    if email_to not in permitted_recipients:
+        # This will be checked again by tasks/vote.py for extra safety
+        raise base.ASFQuartException("Invalid mailing list choice", 
errorcode=400)
+
+    # Check for ongoing tasks
+    ongoing_tasks = await interaction.tasks_ongoing(project_name, 
version_name, selected_revision_number)
+    if ongoing_tasks > 0:
+        return await session.redirect(
+            selected_revision,
+            project_name=project_name,
+            version_name=version_name,
+            revision=selected_revision_number,
+            error="All checks must be completed before starting a vote.",
+        )
+
+    # This sets the phase to RELEASE_CANDIDATE
+    error = await _promote(data, release.name, selected_revision_number)
+    if error:
+        return await session.redirect(root.index, error=error)
+
+    # Store when the release was put into the voting phase
+    release.vote_started = datetime.datetime.now(datetime.UTC)
+
+    # TODO: We also need to store the duration of the vote
+    # We can't allow resolution of the vote until the duration has elapsed
+    # But we allow the user to specify in the form
+    # And yet we also have ReleasePolicy.min_hours
+    # Presumably this sets the default, and the form takes precedence?
+    # ReleasePolicy.min_hours can also be 0, though
+
+    # Create a task for vote initiation
+    task = models.Task(
+        status=models.TaskStatus.QUEUED,
+        task_type=models.TaskType.VOTE_INITIATE,
+        task_args=tasks_vote.Initiate(
+            release_name=release.name,
+            email_to=email_to,
+            vote_duration=vote_duration_choice,
+            initiator_id=session.uid,
+            initiator_fullname=session.fullname,
+            subject=subject_data,
+            body=body_data,
+        ).model_dump(),
+        project_name=project_name,
+        version_name=version,
+    )
+    data.add(task)
+    await data.commit()
+
+    # TODO: We should log all outgoing email and the session so that users can 
confirm
+    # And can be warned if there was a failure
+    # (The message should be shown on the vote resolution page)
+    return await session.redirect(
+        vote.selected,
+        success=f"The vote announcement email will soon be sent to 
{email_to}.",
+        project_name=project_name,
+        version_name=version,
+    )
diff --git a/atr/util.py b/atr/util.py
index 3a73356..4bed063 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -470,6 +470,19 @@ async def get_urls_as_completed(urls: Sequence[str]) -> 
AsyncGenerator[tuple[str
                 yield (url, str(e), b"")
 
 
+async def has_files(release: models.Release) -> bool:
+    """Check if a release has any files."""
+    base_dir = release_directory(release)
+    try:
+        async for rel_path in paths_recursive(base_dir):
+            full_path = base_dir / rel_path
+            if await aiofiles.os.path.isfile(full_path):
+                return True
+    except FileNotFoundError:
+        ...
+    return False
+
+
 async def is_dir_resolve(path: pathlib.Path) -> pathlib.Path | None:
     try:
         resolved_path = await asyncio.to_thread(path.resolve)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to