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 7ca36bd  Reserve a type for confirmation fields and make them more 
consistent
7ca36bd is described below

commit 7ca36bd54b6c35c28f9e86f87b049fd6c0653a79
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Dec 23 16:14:37 2025 +0000

    Reserve a type for confirmation fields and make them more consistent
---
 atr/admin/__init__.py                 | 18 ++-------------
 atr/form.py                           |  3 +++
 atr/get/announce.py                   |  2 +-
 atr/shared/announce.py                | 13 +++++------
 atr/static/js/src/announce-confirm.js | 43 +++++++++++++++++++++++++++++++++++
 playwright/test.py                    | 19 ++++++++++------
 tests/e2e/announce/test_get.py        | 20 ++++++++++++++++
 7 files changed, 87 insertions(+), 31 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index 85010f4..63b5a62 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -68,26 +68,12 @@ class BrowseAsUserForm(form.Form):
 
 class DeleteCommitteeKeysForm(form.Form):
     committee_name: str = form.label("Committee", widget=form.Widget.SELECT)
-    confirm_delete: str = form.label("Confirmation", "Type DELETE KEYS to 
confirm")
-
-    @pydantic.field_validator("confirm_delete")
-    @classmethod
-    def validate_confirm_delete(cls, v: str) -> str:
-        if v != "DELETE KEYS":
-            raise ValueError("You must type DELETE KEYS exactly to confirm 
deletion")
-        return v
+    confirm_delete: Literal["DELETE KEYS"] = form.label("Confirmation", "Type 
DELETE KEYS to confirm.")
 
 
 class DeleteReleaseForm(form.Form):
     releases_to_delete: form.StrList = form.label("Select releases to delete", 
widget=form.Widget.CUSTOM)
-    confirm_delete: str = form.label("Confirmation", "Please type DELETE 
exactly to confirm deletion.")
-
-    @pydantic.field_validator("confirm_delete")
-    @classmethod
-    def validate_confirm_delete(cls, v: str) -> str:
-        if v != "DELETE":
-            raise ValueError("You must type DELETE exactly to confirm 
deletion")
-        return v
+    confirm_delete: Literal["DELETE"] = form.label("Confirmation", "Type 
DELETE to confirm.")
 
     @pydantic.field_validator("releases_to_delete")
     @classmethod
diff --git a/atr/form.py b/atr/form.py
index 78292bc..97566a6 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -681,6 +681,9 @@ def _get_widget_type(field_info: pydantic.fields.FieldInfo) 
-> Widget:  # noqa:
         return Widget.NUMBER
 
     if origin is Literal:
+        args = get_args(annotation)
+        if len(args) == 1:
+            return Widget.TEXT
         return Widget.SELECT
 
     if origin is set:
diff --git a/atr/get/announce.py b/atr/get/announce.py
index 1ff7327..1025aee 100644
--- a/atr/get/announce.py
+++ b/atr/get/announce.py
@@ -82,7 +82,7 @@ async def selected(session: web.Committer, project_name: str, 
version_name: str)
         title=f"Announce and distribute {release.project.display_name} 
{release.version}",
         description=f"Announce and distribute {release.project.display_name} 
{release.version} as a release.",
         content=content,
-        javascripts=["announce-preview", "copy-variable"],
+        javascripts=["announce-confirm", "announce-preview", "copy-variable"],
     )
 
 
diff --git a/atr/shared/announce.py b/atr/shared/announce.py
index e844841..7d208fd 100644
--- a/atr/shared/announce.py
+++ b/atr/shared/announce.py
@@ -15,6 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
+from typing import Literal
+
 import pydantic
 
 import atr.form as form
@@ -31,7 +33,10 @@ class AnnounceForm(form.Form):
     subject: str = form.label("Subject")
     body: str = form.label("Body", widget=form.Widget.CUSTOM)
     download_path_suffix: str = form.label("Download path suffix", 
widget=form.Widget.CUSTOM)
-    confirm_announce: form.Bool = form.label("Confirm")
+    confirm_announce: Literal["CONFIRM"] = form.label(
+        "Confirm",
+        "Type CONFIRM (in capitals) to enable the submit button.",
+    )
 
     @pydantic.field_validator("download_path_suffix")
     @classmethod
@@ -49,9 +54,3 @@ class AnnounceForm(form.Form):
         if "/." in suffix:
             raise ValueError("Download path suffix must not contain /.")
         return suffix
-
-    @pydantic.model_validator(mode="after")
-    def validate_confirm(self) -> "AnnounceForm":
-        if not self.confirm_announce:
-            raise ValueError("You must confirm the announcement by checking 
the box")
-        return self
diff --git a/atr/static/js/src/announce-confirm.js 
b/atr/static/js/src/announce-confirm.js
new file mode 100644
index 0000000..f33177a
--- /dev/null
+++ b/atr/static/js/src/announce-confirm.js
@@ -0,0 +1,43 @@
+/*
+ *  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.
+ */
+
+function initAnnounceConfirm() {
+       const confirmInput = document.getElementById("confirm_announce");
+       const announceForm = document.querySelector("form.atr-canary");
+
+       if (!confirmInput || !announceForm) {
+               return;
+       }
+
+       const submitButton = 
announceForm.querySelector('button[type="submit"]');
+       if (!submitButton) {
+               return;
+       }
+
+       const updateButtonState = () => {
+               const isConfirmed = confirmInput.value === "CONFIRM";
+               submitButton.disabled = !isConfirmed;
+       };
+
+       confirmInput.addEventListener("input", updateButtonState);
+
+       updateButtonState();
+}
+
+document.addEventListener("DOMContentLoaded", initAnnounceConfirm);
diff --git a/playwright/test.py b/playwright/test.py
index bbca524..ebe6fc2 100755
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -274,16 +274,21 @@ def lifecycle_06_announce_preview(page: Page, 
credentials: Credentials, version_
     form_locator = 
page.locator(f'form[action="/announce/{TEST_PROJECT}/{esc_id(version_name)}"]')
     expect(form_locator).to_be_visible()
 
-    logging.info("Locating the confirmation checkbox within the form")
-    checkbox_locator = form_locator.locator('input[name="confirm_announce"]')
-    expect(checkbox_locator).to_be_visible()
+    logging.info("Locating the confirmation input within the form")
+    confirm_input_locator = 
form_locator.locator('input[name="confirm_announce"]')
+    expect(confirm_input_locator).to_be_visible()
 
-    logging.info("Checking the confirmation checkbox")
-    checkbox_locator.check()
-
-    logging.info("Locating and activating the announce button within the form")
+    logging.info("Verifying submit button is initially disabled")
     submit_button_locator = form_locator.get_by_role("button", name="Send 
announcement email")
+    expect(submit_button_locator).to_be_disabled()
+
+    logging.info("Typing CONFIRM in the confirmation input")
+    confirm_input_locator.fill("CONFIRM")
+
+    logging.info("Verifying submit button is now enabled")
     expect(submit_button_locator).to_be_enabled()
+
+    logging.info("Clicking the announce button")
     submit_button_locator.click()
 
     logging.info("Waiting for navigation to /releases after submitting 
announcement")
diff --git a/tests/e2e/announce/test_get.py b/tests/e2e/announce/test_get.py
index 6e8aa64..65731e1 100644
--- a/tests/e2e/announce/test_get.py
+++ b/tests/e2e/announce/test_get.py
@@ -103,3 +103,23 @@ def test_preview_updates_on_body_input(page_announce: 
Page) -> None:
 
     preview_tab.click()
     expect(preview_content).not_to_have_text(initial_preview or "")
+
+
+def test_submit_button_disabled_until_confirm_typed(page_announce: Page) -> 
None:
+    """The submit button should be disabled until CONFIRM is typed."""
+    submit_button = page_announce.get_by_role("button", name="Send 
announcement email")
+    confirm_input = page_announce.locator("#confirm_announce")
+
+    expect(submit_button).to_be_disabled()
+
+    confirm_input.fill("confirm")
+    expect(submit_button).to_be_disabled()
+
+    confirm_input.fill("CONFIRM")
+    expect(submit_button).to_be_enabled()
+
+    confirm_input.fill("CONFIRME")
+    expect(submit_button).to_be_disabled()
+
+    confirm_input.fill("CONFIRM")
+    expect(submit_button).to_be_enabled()


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

Reply via email to