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/07/31 14:26:10 UTC
(tooling-trusted-release) branch main updated: Simplify some forms
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 <se...@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. user@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. user@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-tests@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-tests@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-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org