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 8124503  Make the form to cast a vote more type safe
8124503 is described below

commit 812450348947bab27ce0d5dff5df261be9800dab
Author: Sean B. Palmer <[email protected]>
AuthorDate: Sun Nov 9 12:51:20 2025 +0000

    Make the form to cast a vote more type safe
---
 atr/form.py                                       |  2 +-
 atr/get/vote.py                                   | 35 +++++++++----
 atr/post/vote.py                                  | 64 ++++++-----------------
 atr/shared/__init__.py                            |  3 +-
 atr/shared/vote.py                                | 11 ++--
 atr/templates/check-selected-candidate-forms.html | 36 +------------
 6 files changed, 50 insertions(+), 101 deletions(-)

diff --git a/atr/form.py b/atr/form.py
index dec43a5..6635104 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -199,7 +199,7 @@ async def render(
     submit_classes: str = "btn-primary",
     submit_label: str = "Submit",
     cancel_url: str | None = None,
-    textarea_rows: int = 18,
+    textarea_rows: int = 12,
     defaults: dict[str, Any] | None = None,
     errors: dict[str, list[str]] | None = None,
     use_error_data: bool = True,
diff --git a/atr/get/vote.py b/atr/get/vote.py
index 9cd6b3a..f21c8e8 100644
--- a/atr/get/vote.py
+++ b/atr/get/vote.py
@@ -16,10 +16,12 @@
 # under the License.
 
 import asfquart.base as base
+import htpy
 
 import atr.blueprints.get as get
 import atr.db as db
 import atr.db.interaction as interaction
+import atr.form as form
 import atr.forms as forms
 import atr.log as log
 import atr.mapping as mapping
@@ -88,9 +90,8 @@ async def selected(session: web.Committer | None, 
project_name: str, version_nam
         resolve_form = await forms.Submit.create_form()
         resolve_form.submit.label.text = "Resolve vote"
 
-    form = None
+    cast_vote_form = None
     if can_vote:
-        form = await shared.vote.CastVoteForm.create_form()
         async with storage.write() as write:
             try:
                 if release.committee.is_podling:
@@ -100,20 +101,34 @@ async def selected(session: web.Committer | None, 
project_name: str, version_nam
                 potency = "Binding"
             except storage.AccessError:
                 potency = "Non-binding"
-        forms.choices(
-            form.vote_value,
-            choices=[
-                ("+1", f"+1 ({potency})"),
-                ("0", "0"),
-                ("-1", f"-1 ({potency})"),
-            ],
+
+        vote_widget = htpy.div(class_="btn-group", role="group")[
+            htpy.input(
+                type="radio", class_="btn-check", name="decision", 
id="decision_0", value="+1", autocomplete="off"
+            ),
+            htpy.label(class_="btn btn-outline-success", 
for_="decision_0")[f"+1 ({potency})"],
+            htpy.input(
+                type="radio", class_="btn-check", name="decision", 
id="decision_1", value="0", autocomplete="off"
+            ),
+            htpy.label(class_="btn btn-outline-secondary", 
for_="decision_1")["0"],
+            htpy.input(
+                type="radio", class_="btn-check", name="decision", 
id="decision_2", value="-1", autocomplete="off"
+            ),
+            htpy.label(class_="btn btn-outline-danger", 
for_="decision_2")[f"-1 ({potency})"],
+        ]
+
+        cast_vote_form = await form.render(
+            model_cls=shared.vote.CastVoteForm,
+            submit_label="Submit vote",
+            form_classes=".atr-canary.py-4.px-5.mb-4.border.rounded",
+            custom={"decision": vote_widget},
         )
 
     return await shared.check(
         session,
         release,
         task_mid=task_mid,
-        form=form,
+        form=cast_vote_form,
         resolve_form=resolve_form,
         archive_url=archive_url,
         vote_task=latest_vote_task,
diff --git a/atr/post/vote.py b/atr/post/vote.py
index 81afd69..cdbb5ac 100644
--- a/atr/post/vote.py
+++ b/atr/post/vote.py
@@ -18,67 +18,35 @@
 import quart
 
 import atr.blueprints.post as post
-import atr.forms as forms
-import atr.get.vote as get_vote
+import atr.get as get
 import atr.models.sql as sql
-import atr.shared.vote as shared_vote
+import atr.shared as shared
 import atr.storage as storage
 import atr.web as web
 
 
 @post.committer("/vote/<project_name>/<version_name>")
-async def selected_post(session: web.Committer, project_name: str, 
version_name: str) -> web.WerkzeugResponse:
-    """Handle submission of a vote."""
[email protected](shared.vote.CastVoteForm)
+async def selected_post(
+    session: web.Committer, cast_vote_form: shared.vote.CastVoteForm, 
project_name: str, version_name: str
+) -> web.WerkzeugResponse:
     await session.check_access(project_name)
 
-    # Ensure the release exists and is in the correct phase
     release = await session.release(project_name, version_name, 
phase=sql.ReleasePhase.RELEASE_CANDIDATE)
 
     if release.committee is None:
         raise ValueError("Release has no committee")
 
-    # Set up form choices
-    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:
-            # Participant, due to session.check_access above
-            potency = "Non-binding"
+    vote = cast_vote_form.decision
+    comment = cast_vote_form.comment
 
-    form = await shared_vote.CastVoteForm.create_form(data=await 
quart.request.form)
-    forms.choices(
-        form.vote_value,
-        choices=[
-            ("+1", f"+1 ({potency})"),
-            ("0", "0"),
-            ("-1", f"-1 ({potency})"),
-        ],
-    )
+    async with storage.write_as_committee_participant(release.committee.name) 
as wacm:
+        email_recipient, error_message = await 
wacm.vote.send_user_vote(release, vote, comment, session.fullname)
 
-    if await form.validate_on_submit():
-        vote = str(form.vote_value.data)
-        comment = str(form.vote_comment.data)
-        async with 
storage.write_as_committee_participant(release.committee.name) as wacm:
-            email_recipient, error_message = await 
wacm.vote.send_user_vote(release, vote, comment, session.fullname)
-        if error_message:
-            return await session.redirect(
-                get_vote.selected, project_name=project_name, 
version_name=version_name, error=error_message
-            )
+    if error_message:
+        await quart.flash(error_message, "error")
+        return await session.redirect(get.vote.selected, 
project_name=project_name, version_name=version_name)
 
-        success_message = f"Sending your vote to {email_recipient}."
-        return await session.redirect(
-            get_vote.selected, project_name=project_name, 
version_name=version_name, success=success_message
-        )
-    else:
-        error_message = "Invalid vote submission"
-        if form.errors:
-            error_details = "; ".join([f"{field}: {', '.join(errs)}" for 
field, errs in form.errors.items()])
-            error_message = f"{error_message}: {error_details}"
-
-        return await session.redirect(
-            get_vote.selected, project_name=project_name, 
version_name=version_name, error=error_message
-        )
+    success_message = f"Sending your vote to {email_recipient}."
+    await quart.flash(success_message, "success")
+    return await session.redirect(get.vote.selected, 
project_name=project_name, version_name=version_name)
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index 1426353..e5d7825 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -22,6 +22,7 @@ import wtforms
 import atr.db as db
 import atr.db.interaction as interaction
 import atr.forms as forms
+import atr.htm as htm
 import atr.models.results as results
 import atr.models.sql as sql
 import atr.shared.announce as announce
@@ -78,7 +79,7 @@ async def check(
     session: web.Committer | None,
     release: sql.Release,
     task_mid: str | None = None,
-    form: wtforms.Form | None = None,
+    form: htm.Element | None = None,
     resolve_form: wtforms.Form | None = None,
     archive_url: str | None = None,
     vote_task: sql.Task | None = None,
diff --git a/atr/shared/vote.py b/atr/shared/vote.py
index 0738790..fb30afb 100644
--- a/atr/shared/vote.py
+++ b/atr/shared/vote.py
@@ -15,12 +15,11 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import atr.forms as forms
+from typing import Literal
 
+import atr.form as form
 
-class CastVoteForm(forms.Typed):
-    """Form for casting a vote."""
 
-    vote_value = forms.radio("Your vote")
-    vote_comment = forms.textarea("Comment (optional)", optional=True)
-    submit = forms.submit("Submit vote")
+class CastVoteForm(form.Form):
+    decision: Literal["+1", "0", "-1"] = form.label("Your vote", 
widget=form.Widget.CUSTOM)
+    comment: str = form.label("Comment (optional)", 
widget=form.Widget.TEXTAREA)
diff --git a/atr/templates/check-selected-candidate-forms.html 
b/atr/templates/check-selected-candidate-forms.html
index 2e2b4e4..cdf7b60 100644
--- a/atr/templates/check-selected-candidate-forms.html
+++ b/atr/templates/check-selected-candidate-forms.html
@@ -6,39 +6,5 @@
 {% if can_vote and form %}
   <h2>Cast your vote</h2>
 
-  <form method="post"
-        action="{{ as_url(post.vote.selected_post, project_name=project_name, 
version_name=version_name) }}"
-        class="atr-canary py-4 px-5 mb-4 border rounded">
-    {{ form.hidden_tag() }}
-
-    <div class="row mb-3 pb-3 border-bottom">
-      {{ forms.label(form.vote_value, col="md3") }}
-      <div class="col-md-9">
-        <div class="btn-group" role="group" aria-label="Vote options">
-          {% for subfield in form.vote_value %}
-            {% set btn_class = "btn-outline-secondary" %}
-            {% if subfield.data == "+1" %}
-              {% set btn_class = "btn-outline-success" %}
-            {% endif %}
-            {% if subfield.data == "-1" %}
-              {% set btn_class = "btn-outline-danger" %}
-            {% endif %}
-            {{ forms.widget(subfield, classes="btn-check", autocomplete="off") 
}}
-            {{ forms.label(subfield, classes="btn " + btn_class) }}
-          {% endfor %}
-        </div>
-        {{ forms.errors(form.vote_value, classes="text-danger small mt-1") }}
-      </div>
-    </div>
-    <div class="row mb-3">
-      {{ forms.label(form.vote_comment, col="md3") }}
-      <div class="col-md-9">
-        {{ forms.widget(form.vote_comment, rows="3") }}
-        {{ forms.errors(form.vote_comment, classes="text-danger small mt-1") }}
-      </div>
-    </div>
-    <div class="row">
-      <div class="col-md-9 offset-md-3">{{ form.submit(class_="btn 
btn-primary") }}</div>
-    </div>
-  </form>
+  {{ form }}
 {% endif %}


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

Reply via email to