This is an automated email from the ASF dual-hosted git repository. sbp pushed a commit to branch sbp in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 682ff1796efcc73b01ce2edc53e57925625ae953 Author: Sean B. Palmer <[email protected]> AuthorDate: Tue Mar 24 17:30:23 2026 +0000 Add a form to tally any vote --- atr/get/user.py | 16 ++++++++ atr/post/user.py | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++ atr/shared/user.py | 4 ++ 3 files changed, 126 insertions(+) diff --git a/atr/get/user.py b/atr/get/user.py index 2f3f3541..61653451 100644 --- a/atr/get/user.py +++ b/atr/get/user.py @@ -86,3 +86,19 @@ async def cache_get(session: web.Committer, _user_cache: Literal["user/cache"]) block.append(cache_form) return await template.blank("Session cache management", content=block.collect()) + + [email protected] +async def tally(session: web.Committer, _user_tally: Literal["user/tally"]) -> str: + """ + URL: /user/tally + """ + block = htm.Block() + block.h1["Vote tally"] + block.p["Enter a lists.apache.org thread URL or thread ID to count the votes in the thread."] + tally_form = form.render( + model_cls=shared.user.TallyForm, + submit_label="Count votes", + ) + block.append(tally_form) + return await template.blank("Vote tally", content=block.collect()) diff --git a/atr/post/user.py b/atr/post/user.py index 06ec280a..ffc93747 100644 --- a/atr/post/user.py +++ b/atr/post/user.py @@ -19,8 +19,13 @@ from typing import Literal import quart import atr.blueprints.post as post +import atr.form as form import atr.get as get +import atr.htm as htm +import atr.models as models import atr.shared as shared +import atr.tabulate as tabulate +import atr.template as template import atr.util as util import atr.web as web @@ -44,6 +49,52 @@ async def session_post( return await session.redirect(get.user.cache_get) [email protected] +async def tally( + session: web.Committer, + _user_tally: Literal["user/tally"], + tally_form: shared.user.TallyForm, +) -> str: + """ + URL: /user/tally + """ + thread_id = _extract_thread_id(tally_form.thread) + + block = htm.Block() + block.h1["Vote tally"] + + tally_resubmit = form.render( + model_cls=shared.user.TallyForm, + submit_label="Count votes", + defaults={"thread": tally_form.thread}, + ) + block.append(tally_resubmit) + + if not thread_id: + block.div(".alert.alert-danger")["Please enter a thread URL or ID."] + return await template.blank("Vote tally", content=block.collect()) + + try: + _start_unixtime, tabulated_votes = await tabulate.votes(None, thread_id) + except (util.FetchError, ValueError) as e: + block.div(".alert.alert-danger")[str(e)] + return await template.blank("Vote tally", content=block.collect()) + + if not tabulated_votes: + block.p["No votes found in this thread."] + return await template.blank("Vote tally", content=block.collect()) + + summary = tabulate.vote_summary(tabulated_votes) + + block.h2["Votes"] + _render_votes_table(block, tabulated_votes) + + block.h2["Summary"] + _render_summary_table(block, summary) + + return await template.blank("Vote tally", content=block.collect()) + + async def _cache_session(session: web.Committer) -> None: cache_data = await util.session_cache_read() @@ -73,3 +124,58 @@ async def _delete_session_cache(session: web.Committer) -> None: if session.uid in cache_data: del cache_data[session.uid] await util.session_cache_write(cache_data) + + +def _extract_thread_id(value: str) -> str: + value = value.strip().rstrip("/") + marker = "lists.apache.org/thread/" + index = value.find(marker) + if index >= 0: + return value[index + len(marker) :] + return value + + +def _render_summary_table(block: htm.Block, summary: dict[str, int]) -> None: + thead = htm.thead[htm.tr[htm.th["Category"], htm.th["Yes"], htm.th["No"], htm.th["Abstain"], htm.th["Total"]]] + tbody = htm.Block(htm.tbody) + for label, prefix in [("Binding", "binding"), ("Non-binding", "non_binding"), ("Unknown", "unknown")]: + total = summary[f"{prefix}_votes"] + if total == 0: + continue + tbody.append( + htm.tr[ + htm.td[label], + htm.td[str(summary[f"{prefix}_votes_yes"])], + htm.td[str(summary[f"{prefix}_votes_no"])], + htm.td[str(summary[f"{prefix}_votes_abstain"])], + htm.td[str(total)], + ] + ) + block.table(".table.table-striped")[thead, tbody.collect()] + + +def _render_votes_table(block: htm.Block, tabulated_votes: dict[str, models.tabulate.VoteEmail]) -> None: + thead = htm.thead[ + htm.tr[ + htm.th["UID or email"], + htm.th(".text-center")["Vote"], + htm.th(".text-center")["Status"], + htm.th["Quotation"], + ] + ] + tbody = htm.Block(htm.tbody) + for vote_email in tabulated_votes.values(): + vote_class = "" + if vote_email.vote == models.tabulate.Vote.YES: + vote_class = ".atr-green" + elif vote_email.vote == models.tabulate.Vote.NO: + vote_class = ".atr-red" + tbody.append( + htm.tr[ + htm.td(".atr-nowrap")[vote_email.asf_uid_or_email], + htm.td(f".atr-nowrap.text-center{vote_class}")[vote_email.vote.value], + htm.td(".atr-nowrap.text-center")[vote_email.status.value], + htm.td[vote_email.quotation], + ] + ) + block.table(".table.table-striped")[thead, tbody.collect()] diff --git a/atr/shared/user.py b/atr/shared/user.py index 8e321f8f..d3e07064 100644 --- a/atr/shared/user.py +++ b/atr/shared/user.py @@ -31,6 +31,10 @@ class DeleteCacheForm(form.Empty): variant: DELETE = form.value(DELETE) +class TallyForm(form.Form): + thread: str = form.label("Thread URL or ID") + + type UserCacheForm = Annotated[ CacheUserForm | DeleteCacheForm, form.DISCRIMINATOR, --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
