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 9ec25ab  Simplify some forms
9ec25ab is described below

commit 9ec25ab6b5279ca5c96183441c51b11227ad1a80
Author: Sean B. Palmer <s...@miscoranda.com>
AuthorDate: Thu Jul 31 15:25:55 2025 +0100

    Simplify some forms
---
 atr/blueprints/admin/admin.py | 42 ++++++------------
 atr/forms.py                  | 99 +++++++++++++++++++++++++++++++++++++++++--
 atr/routes/announce.py        | 71 +++++++++++--------------------
 3 files changed, 133 insertions(+), 79 deletions(-)

diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 50b3337..841c3ea 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -34,7 +34,6 @@ import quart
 import sqlalchemy.orm as orm
 import sqlmodel
 import werkzeug.wrappers.response as response
-import wtforms
 
 import atr.blueprints.admin as admin
 import atr.config as config
@@ -56,49 +55,36 @@ import atr.validate as validate
 class BrowseAsUserForm(forms.Typed):
     """Form for browsing as another user."""
 
-    uid = wtforms.StringField(
-        "ASF UID",
-        validators=[wtforms.validators.InputRequired()],
-        render_kw={"placeholder": "Enter the ASF UID to browse as"},
-    )
-    submit = wtforms.SubmitField("Browse as this user")
+    uid = forms.string("ASF UID", placeholder="Enter the ASF UID to browse as")
+    submit = forms.submit("Browse as this user")
 
 
 class DeleteCommitteeKeysForm(forms.Typed):
-    committee_name = wtforms.SelectField("Committee", 
validators=[wtforms.validators.InputRequired()])
-    confirm_delete = wtforms.StringField(
+    committee_name = forms.select("Committee")
+    confirm_delete = forms.string(
         "Confirmation",
-        validators=[wtforms.validators.InputRequired(), 
wtforms.validators.Regexp("^DELETE KEYS$")],
-        render_kw={"placeholder": "DELETE KEYS"},
+        validators=forms.constant("DELETE KEYS"),
+        placeholder="DELETE KEYS",
     )
-    submit = wtforms.SubmitField("Delete all keys for selected committee")
+    submit = forms.submit("Delete all keys for selected committee")
 
 
 class DeleteReleaseForm(forms.Typed):
     """Form for deleting releases."""
 
-    confirm_delete = wtforms.StringField(
+    confirm_delete = forms.string(
         "Confirmation",
-        validators=[
-            wtforms.validators.InputRequired("Confirmation is required"),
-            wtforms.validators.Regexp("^DELETE$", message="Please type DELETE 
to confirm"),
-        ],
-        render_kw={"placeholder": "DELETE"},
+        validators=forms.constant("DELETE"),
+        placeholder="DELETE",
         description="Please type DELETE exactly to confirm deletion.",
     )
-    submit = wtforms.SubmitField("Delete selected releases permanently")
+    submit = forms.submit("Delete selected releases permanently")
 
 
 class LdapLookupForm(forms.Typed):
-    uid = wtforms.StringField(
-        "ASF UID (optional)",
-        render_kw={"placeholder": "Enter ASF UID, e.g. johnsmith, or * for 
all"},
-    )
-    email = wtforms.StringField(
-        "Email address (optional)",
-        render_kw={"placeholder": "Enter email address, e.g. 
u...@example.org"},
-    )
-    submit = wtforms.SubmitField("Lookup")
+    uid = forms.string("ASF UID (optional)", placeholder="Enter ASF UID, e.g. 
johnsmith, or * for all")
+    email = forms.string("Email address (optional)", placeholder="Enter email 
address, e.g. u...@example.org")
+    submit = forms.submit("Lookup")
 
 
 @admin.BLUEPRINT.route("/all-releases")
diff --git a/atr/forms.py b/atr/forms.py
index 180aa54..e46d835 100644
--- a/atr/forms.py
+++ b/atr/forms.py
@@ -17,12 +17,21 @@
 
 from __future__ import annotations
 
-from typing import Any, TypeVar
+from typing import Any, Final, TypeVar
 
 import quart_wtf
 import quart_wtf.typing
 import wtforms
 
+REQUIRED: Final = wtforms.validators.InputRequired()
+REQUIRED_DATA: Final = wtforms.validators.DataRequired()
+OPTIONAL: Final = wtforms.validators.Optional()
+
+# Match _Choice in the wtforms.fields.choices stub
+# typeshed-fallback/stubs/WTForms/wtforms/fields/choices.pyi
+type Choice = tuple[Any, str] | tuple[Any, str, dict[str, Any]]
+type Choices = list[Choice]
+
 
 class Typed(quart_wtf.QuartForm):
     """Quart form with type annotations."""
@@ -59,12 +68,94 @@ class Hidden(Typed):
 
 
 class Value(Typed):
-    value = 
wtforms.StringField(validators=[wtforms.validators.InputRequired()])
+    value = wtforms.StringField(validators=[REQUIRED])
     submit = wtforms.SubmitField()
 
 
+def boolean(
+    label: str, optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any
+) -> wtforms.BooleanField:
+    if validators is None:
+        validators = []
+    if optional is False:
+        validators.append(REQUIRED_DATA)
+    else:
+        validators.append(OPTIONAL)
+    return wtforms.BooleanField(label, **kwargs)
+
+
+def choices(field: wtforms.RadioField, choices: Choices, default: str | None = 
None) -> None:
+    field.choices = choices
+    # Form construction calls Field.process
+    # This sets data = self.default() or self.default
+    # Then self.object_data = data
+    # Then calls self.process_data(data) which sets self.data = data
+    # And SelectField.iter_choices reads self.data for the default
+    if default is not None:
+        field.data = default
+
+
+def constant(value: str) -> list[wtforms.validators.InputRequired | 
wtforms.validators.Regexp]:
+    return [REQUIRED, wtforms.validators.Regexp(value, message=f"You must 
enter {value!r} in this field")]
+
+
+def hidden(**kwargs: Any) -> wtforms.HiddenField:
+    return wtforms.HiddenField(**kwargs)
+
+
+def optional(label: str, **kwargs: Any) -> wtforms.StringField:
+    return string(label, optional=True, **kwargs)
+
+
+def radio(label: str, optional: bool = False, validators: list[Any] | None = 
None, **kwargs: Any) -> wtforms.RadioField:
+    # Choices and default must be set at runtime
+    if validators is None:
+        validators = []
+    if optional is False:
+        validators.append(REQUIRED)
+    else:
+        validators.append(OPTIONAL)
+    return wtforms.RadioField(label, validators=validators, **kwargs)
+
+
+def select(label: str, validators: list[Any] | None = None, **kwargs: Any) -> 
wtforms.SelectField:
+    if validators is None:
+        validators = [REQUIRED]
+    return wtforms.SelectField(label, validators=validators, **kwargs)
+
+
 # TODO: No shared class for Validators?
-def string(label: str, validators: list[Any] | None = None, **kwargs: Any) -> 
wtforms.StringField:
+def string(
+    label: str,
+    optional: bool = False,
+    validators: list[Any] | None = None,
+    placeholder: str | None = None,
+    **kwargs: Any,
+) -> wtforms.StringField:
     if validators is None:
-        validators = [wtforms.validators.InputRequired()]
+        validators = []
+    if optional is False:
+        validators.append(REQUIRED)
+    else:
+        validators.append(OPTIONAL)
+    if placeholder is not None:
+        if "render_kw" not in kwargs:
+            kwargs["render_kw"] = {}
+        kwargs["render_kw"]["placeholder"] = placeholder
     return wtforms.StringField(label, validators=validators, **kwargs)
+
+
+def submit(label: str, **kwargs: Any) -> wtforms.SubmitField:
+    return wtforms.SubmitField(label, **kwargs)
+
+
+def textarea(
+    label: str, optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any
+) -> wtforms.TextAreaField:
+    if validators is None:
+        validators = []
+    if optional is False:
+        validators.append(REQUIRED)
+    else:
+        validators.append(OPTIONAL)
+    return wtforms.TextAreaField(label, validators=validators, **kwargs)
diff --git a/atr/routes/announce.py b/atr/routes/announce.py
index 5fe28ef..8b97ffe 100644
--- a/atr/routes/announce.py
+++ b/atr/routes/announce.py
@@ -18,14 +18,13 @@
 import asyncio
 import datetime
 import pathlib
-from typing import Any, Protocol
+from typing import Any
 
 import aiofiles.os
 import aioshutil
 import quart
 import sqlmodel
 import werkzeug.wrappers.response as response
-import wtforms
 
 import atr.config as config
 import atr.construct as construct
@@ -45,38 +44,28 @@ class AnnounceError(Exception):
     """Exception for announce errors."""
 
 
-class AnnounceFormProtocol(Protocol):
-    """Protocol for the dynamically generated AnnounceForm."""
+class AnnounceForm(forms.Typed):
+    """Form for announcing a release preview."""
 
-    preview_name: wtforms.HiddenField
-    preview_revision: wtforms.HiddenField
-    mailing_list: wtforms.RadioField
-    confirm_announce: wtforms.BooleanField
-    download_path_suffix: wtforms.StringField
-    subject: wtforms.StringField
-    body: wtforms.TextAreaField
-    submit: wtforms.SubmitField
-
-    @property
-    def errors(self) -> dict[str, Any]: ...
-
-    async def validate_on_submit(self) -> bool: ...
+    preview_name = forms.hidden()
+    preview_revision = forms.hidden()
+    mailing_list = forms.radio("Send vote email to")
+    download_path_suffix = forms.optional("Download path suffix")
+    confirm_announce = forms.boolean("Confirm")
+    subject = forms.optional("Subject")
+    body = forms.textarea("Body", optional=True)
+    submit = forms.submit("Send announcement email")
 
 
 class DeleteForm(forms.Typed):
     """Form for deleting a release preview."""
 
-    preview_name = wtforms.StringField(
-        "Preview name", validators=[wtforms.validators.InputRequired("Preview 
name is required")]
-    )
-    confirm_delete = wtforms.StringField(
+    preview_name = forms.string("Preview name")
+    confirm_delete = forms.string(
         "Confirmation",
-        validators=[
-            wtforms.validators.InputRequired("Confirmation is required"),
-            wtforms.validators.Regexp("^DELETE$", message="Please type DELETE 
to confirm"),
-        ],
+        validators=forms.constant("DELETE"),
     )
-    submit = wtforms.SubmitField("Delete preview")
+    submit = forms.submit("Delete preview")
 
 
 @routes.committer("/announce/<project_name>/<version_name>")
@@ -272,34 +261,22 @@ async def announce(
 
 async def _create_announce_form_instance(
     permitted_recipients: list[str], *, data: dict[str, Any] | None = None
-) -> AnnounceFormProtocol:
+) -> AnnounceForm:
     """Create and return an instance of the AnnounceForm."""
 
-    class AnnounceForm(forms.Typed):
-        """Form for announcing a release preview."""
-
-        preview_name = wtforms.HiddenField()
-        preview_revision = wtforms.HiddenField()
-        mailing_list = wtforms.RadioField(
-            "Send vote email to",
-            choices=sorted([(recipient, recipient) for recipient in 
permitted_recipients]),
-            validators=[wtforms.validators.InputRequired("Mailing list 
selection is required")],
-            default="user-te...@tooling.apache.org",
-        )
-        download_path_suffix = wtforms.StringField("Download path suffix", 
validators=[wtforms.validators.Optional()])
-        confirm_announce = wtforms.BooleanField(
-            "Confirm",
-            validators=[wtforms.validators.DataRequired("You must confirm to 
proceed with announcement")],
-        )
-        subject = wtforms.StringField("Subject", 
validators=[wtforms.validators.Optional()])
-        body = wtforms.TextAreaField("Body", 
validators=[wtforms.validators.Optional()])
-        submit = wtforms.SubmitField("Send announcement email")
+    mailing_list_choices: forms.Choices = sorted([(recipient, recipient) for 
recipient in permitted_recipients])
+    mailing_list_default = "user-te...@tooling.apache.org"
 
     form_instance = await AnnounceForm.create_form(data=data)
+    forms.choices(
+        form_instance.mailing_list,
+        mailing_list_choices,
+        mailing_list_default,
+    )
     return form_instance
 
 
-def _download_path_suffix_validated(announce_form: AnnounceFormProtocol) -> 
str:
+def _download_path_suffix_validated(announce_form: AnnounceForm) -> str:
     download_path_suffix = str(announce_form.download_path_suffix.data)
     if (".." in download_path_suffix) or ("//" in download_path_suffix):
         raise ValueError("Download path suffix must not contain .. or //")


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

Reply via email to