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]