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 e388eaa  Document the available variables in the vote and announce 
forms
e388eaa is described below

commit e388eaad0a25932dfdd3ec1a2e8c24f91b988209
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Dec 4 16:27:44 2025 +0000

    Document the available variables in the vote and announce forms
---
 atr/construct.py    |  27 ++++++++++
 atr/get/announce.py |  49 ++----------------
 atr/get/voting.py   |  51 +++----------------
 atr/render.py       | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 180 insertions(+), 89 deletions(-)

diff --git a/atr/construct.py b/atr/construct.py
index 7d3dfee..3a4d389 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import dataclasses
+from typing import Literal
 
 import aiofiles.os
 import quart
@@ -26,6 +27,24 @@ import atr.db.interaction as interaction
 import atr.models.sql as sql
 import atr.util as util
 
+type Context = Literal["vote", "announce"]
+
+TEMPLATE_VARIABLES: list[tuple[str, str, set[Context]]] = [
+    ("COMMITTEE", "Committee display name", {"vote", "announce"}),
+    ("DOWNLOAD_URL", "URL to download the release", {"announce"}),
+    ("DURATION", "Vote duration in hours", {"vote"}),
+    ("KEYS_FILE", "URL to the KEYS file", {"vote"}),
+    ("PROJECT", "Project display name", {"vote", "announce"}),
+    ("RELEASE_CHECKLIST", "Release checklist content", {"vote"}),
+    ("REVIEW_URL", "URL to review the release", {"vote"}),
+    ("REVISION", "Revision number", {"vote", "announce"}),
+    ("TAG", "Revision tag, if set", {"vote", "announce"}),
+    ("VERSION", "Version name", {"vote", "announce"}),
+    ("VOTE_ENDS_UTC", "Vote end date and time in UTC", {"vote"}),
+    ("YOUR_ASF_ID", "Your Apache UID", {"vote", "announce"}),
+    ("YOUR_FULL_NAME", "Your full name", {"vote", "announce"}),
+]
+
 
 @dataclasses.dataclass
 class AnnounceReleaseOptions:
@@ -100,6 +119,10 @@ async def announce_release_default(project_name: str) -> 
str:
     return project.policy_announce_release_template
 
 
+def announce_template_variables() -> list[tuple[str, str]]:
+    return [(name, desc) for (name, desc, contexts) in TEMPLATE_VARIABLES if 
"announce" in contexts]
+
+
 async def start_vote_body(body: str, options: StartVoteOptions) -> str:
     async with db.session() as data:
         # Do not limit by phase, as it may be at RELEASE_CANDIDATE here if 
called by the task
@@ -166,3 +189,7 @@ async def start_vote_default(project_name: str) -> str:
         )
 
     return project.policy_start_vote_template
+
+
+def vote_template_variables() -> list[tuple[str, str]]:
+    return [(name, desc) for (name, desc, contexts) in TEMPLATE_VARIABLES if 
"vote" in contexts]
diff --git a/atr/get/announce.py b/atr/get/announce.py
index b3ab3dd..b8034c3 100644
--- a/atr/get/announce.py
+++ b/atr/get/announce.py
@@ -27,6 +27,7 @@ import atr.form as form
 import atr.htm as htm
 import atr.models.sql as sql
 import atr.post as post
+import atr.render as render
 import atr.shared as shared
 import atr.shared.distribution as distribution
 import atr.template as template
@@ -168,49 +169,7 @@ def _render_release_card(release: sql.Release) -> 
htm.Element:
 
 def _render_body_tabs(default_body: str) -> htm.Element:
     """Render the tabbed interface for body editing and preview."""
-
-    tabs_ul = htm.ul("#announceBodyTab.nav.nav-tabs", role="tablist")[
-        htm.li(".nav-item", role="presentation")[
-            htpy.button(
-                "#edit-announce-body-tab.nav-link.active",
-                data_bs_toggle="tab",
-                data_bs_target="#edit-announce-body-pane",
-                type="button",
-                role="tab",
-                aria_controls="edit-announce-body-pane",
-                aria_selected="true",
-            )["Edit"]
-        ],
-        htm.li(".nav-item", role="presentation")[
-            htpy.button(
-                "#text-preview-announce-body-tab.nav-link",
-                data_bs_toggle="tab",
-                data_bs_target="#text-preview-announce-body-pane",
-                type="button",
-                role="tab",
-                aria_controls="text-preview-announce-body-pane",
-                aria_selected="false",
-            )["Text preview"]
-        ],
-    ]
-
-    edit_pane = htm.div("#edit-announce-body-pane.tab-pane.fade.show.active", 
role="tabpanel")[
-        htpy.textarea(
-            "#body.form-control.font-monospace.mt-2",
-            name="body",
-            rows="12",
-        )[default_body]
-    ]
-
-    preview_pane = htm.div("#text-preview-announce-body-pane.tab-pane.fade", 
role="tabpanel")[
-        
htm.pre(".mt-2.p-3.bg-light.border.rounded.font-monospace.overflow-auto")[
-            htm.code("#announce-text-preview-content")["Loading preview..."]
-        ]
-    ]
-
-    tab_content = htm.div("#announceBodyTabContent.tab-content")[edit_pane, 
preview_pane]
-
-    return htm.div[tabs_ul, tab_content]
+    return render.body_tabs("announce-body", default_body, 
construct.announce_template_variables())
 
 
 def _render_mailing_list_with_warning(choices: list[tuple[str, str]], 
default_value: str) -> htm.Element:
@@ -282,7 +241,7 @@ def _render_javascript(release: sql.Release, 
download_path_description: str) ->
             const debounceDelay = 500;
 
             const bodyTextarea = document.getElementById("body");
-            const textPreviewContent = 
document.getElementById("announce-text-preview-content");
+            const textPreviewContent = 
document.getElementById("announce-body-preview-content");
             const announceForm = document.querySelector("form.atr-canary");
 
             if (!bodyTextarea || !textPreviewContent || !announceForm) {{
@@ -337,6 +296,8 @@ def _render_javascript(release: sql.Release, 
download_path_description: str) ->
 
             fetchAndUpdateAnnouncePreview();
 
+            {render.copy_javascript()}
+
             const pathInput = document.getElementById("download_path_suffix");
             const pathHelpText = pathInput ? 
pathInput.parentElement.querySelector(".form-text") : null;
 
diff --git a/atr/get/voting.py b/atr/get/voting.py
index 99d5855..0fff05b 100644
--- a/atr/get/voting.py
+++ b/atr/get/voting.py
@@ -30,6 +30,7 @@ import atr.get.keys as keys
 import atr.htm as htm
 import atr.models.sql as sql
 import atr.post as post
+import atr.render as render
 import atr.shared as shared
 import atr.template as template
 import atr.util as util
@@ -65,7 +66,7 @@ async def selected_revision(
 
         revision_obj = await data.revision(release_name=release.name, 
number=revision).get()
         if revision_obj and revision_obj.tag:
-            subject_suffix = f" (tag: {revision_obj.tag})"
+            subject_suffix = f" ({revision_obj.tag})"
         else:
             subject_suffix = f" (revision {revision})"
 
@@ -173,49 +174,7 @@ async def _render_page(
 
 def _render_body_tabs(default_body: str) -> htm.Element:
     """Render the tabbed interface for body editing and preview."""
-
-    tabs_ul = htm.ul("#voteBodyTab.nav.nav-tabs", role="tablist")[
-        htm.li(".nav-item", role="presentation")[
-            htpy.button(
-                "#edit-vote-body-tab.nav-link.active",
-                data_bs_toggle="tab",
-                data_bs_target="#edit-vote-body-pane",
-                type="button",
-                role="tab",
-                aria_controls="edit-vote-body-pane",
-                aria_selected="true",
-            )["Edit"]
-        ],
-        htm.li(".nav-item", role="presentation")[
-            htpy.button(
-                "#text-preview-vote-body-tab.nav-link",
-                data_bs_toggle="tab",
-                data_bs_target="#text-preview-vote-body-pane",
-                type="button",
-                role="tab",
-                aria_controls="text-preview-vote-body-pane",
-                aria_selected="false",
-            )["Text preview"]
-        ],
-    ]
-
-    edit_pane = htm.div("#edit-vote-body-pane.tab-pane.fade.show.active", 
role="tabpanel")[
-        htpy.textarea(
-            "#body.form-control.font-monospace.mt-2",
-            name="body",
-            rows="12",
-        )[default_body]
-    ]
-
-    preview_pane = htm.div("#text-preview-vote-body-pane.tab-pane.fade", 
role="tabpanel")[
-        
htm.pre(".mt-2.p-3.bg-light.border.rounded.font-monospace.overflow-auto")[
-            htm.code("#vote-text-preview-content")["Loading preview..."]
-        ]
-    ]
-
-    tab_content = htm.div("#voteBodyTabContent.tab-content")[edit_pane, 
preview_pane]
-
-    return htm.div[tabs_ul, tab_content]
+    return render.body_tabs("vote-body", default_body, 
construct.vote_template_variables())
 
 
 def _render_javascript(release, min_hours: int) -> htm.Element:
@@ -231,7 +190,7 @@ def _render_javascript(release, min_hours: int) -> 
htm.Element:
 
             const bodyTextarea = document.getElementById("body");
             const voteDurationInput = document.getElementById("vote_duration");
-            const textPreviewContent = 
document.getElementById("vote-text-preview-content");
+            const textPreviewContent = 
document.getElementById("vote-body-preview-content");
             const voteForm = document.querySelector("form.atr-canary");
 
             if (!bodyTextarea || !voteDurationInput || !textPreviewContent || 
!voteForm) {{
@@ -292,6 +251,8 @@ def _render_javascript(release, min_hours: int) -> 
htm.Element:
             }});
 
             fetchAndUpdateVotePreview();
+
+            {render.copy_javascript()}
         }});
     """
 
diff --git a/atr/render.py b/atr/render.py
new file mode 100644
index 0000000..9907e0f
--- /dev/null
+++ b/atr/render.py
@@ -0,0 +1,142 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import htpy
+
+import atr.htm as htm
+
+
+def body_tabs(
+    tab_id_prefix: str,
+    default_body: str,
+    template_variables: list[tuple[str, str]],
+) -> htm.Element:
+    tabs = htm.Block(htm.ul(f"#{tab_id_prefix}-tab.nav.nav-tabs", 
role="tablist"))
+    tabs.li(".nav-item", role="presentation")[
+        htpy.button(
+            f"#edit-{tab_id_prefix}-tab.nav-link.active",
+            data_bs_toggle="tab",
+            data_bs_target=f"#edit-{tab_id_prefix}-pane",
+            type="button",
+            role="tab",
+            aria_controls=f"edit-{tab_id_prefix}-pane",
+            aria_selected="true",
+        )["Edit"]
+    ]
+    tabs.li(".nav-item", role="presentation")[
+        htpy.button(
+            f"#preview-{tab_id_prefix}-tab.nav-link",
+            data_bs_toggle="tab",
+            data_bs_target=f"#preview-{tab_id_prefix}-pane",
+            type="button",
+            role="tab",
+            aria_controls=f"preview-{tab_id_prefix}-pane",
+            aria_selected="false",
+        )["Text preview"]
+    ]
+    tabs.append(variables_tab_button(tab_id_prefix))
+
+    edit_pane = 
htm.div(f"#edit-{tab_id_prefix}-pane.tab-pane.fade.show.active", 
role="tabpanel")[
+        htpy.textarea(
+            "#body.form-control.font-monospace.mt-2",
+            name="body",
+            rows="12",
+        )[default_body]
+    ]
+
+    preview_pane = htm.div(f"#preview-{tab_id_prefix}-pane.tab-pane.fade", 
role="tabpanel")[
+        
htm.pre(".mt-2.p-3.bg-light.border.rounded.font-monospace.overflow-auto")[
+            htm.code(f"#{tab_id_prefix}-preview-content")["Loading preview..."]
+        ]
+    ]
+
+    variables_pane = variables_tab(tab_id_prefix, template_variables)
+
+    tab_content = 
htm.div(f"#{tab_id_prefix}-tab-content.tab-content")[edit_pane, preview_pane, 
variables_pane]
+
+    return htm.div[tabs.collect(), tab_content]
+
+
+def copy_javascript() -> str:
+    # TODO: We need to ensure that all JS files are standalone
+    # We have far too many embedded snippets
+    return """
+        document.querySelectorAll(".copy-var-btn").forEach(btn => {
+            btn.addEventListener("click", () => {
+                const variable = btn.dataset.variable;
+                navigator.clipboard.writeText(variable).then(() => {
+                    const originalText = btn.textContent;
+                    btn.textContent = "Copied!";
+                    btn.classList.remove("btn-outline-secondary");
+                    btn.classList.add("btn-success");
+                    setTimeout(() => {
+                        btn.textContent = originalText;
+                        btn.classList.remove("btn-success");
+                        btn.classList.add("btn-outline-secondary");
+                    }, 1500);
+                });
+            });
+        });
+    """
+
+
+def variables_tab(
+    tab_id_prefix: str,
+    template_variables: list[tuple[str, str]],
+) -> htm.Element:
+    variable_rows = []
+    for name, description in template_variables:
+        variable_rows.append(
+            htm.tr[
+                htm.td(".font-monospace.text-nowrap")[f"[{name}]"],
+                htm.td[description],
+                htm.td(".text-end")[
+                    htpy.button(
+                        ".btn.btn-sm.btn-outline-secondary.copy-var-btn",
+                        type="button",
+                        data_variable=f"[{name}]",
+                    )["Copy"]
+                ],
+            ]
+        )
+
+    variables_table = htm.table(".table.table-sm.mt-2")[
+        htm.thead[
+            htm.tr[
+                htm.th["Variable"],
+                htm.th["Description"],
+                htm.th[""],
+            ]
+        ],
+        htm.tbody[*variable_rows],
+    ]
+
+    return htm.div(f"#{tab_id_prefix}-variables-pane.tab-pane.fade", 
role="tabpanel")[variables_table]
+
+
+def variables_tab_button(tab_id_prefix: str) -> htm.Element:
+    return htm.li(".nav-item", role="presentation")[
+        htpy.button(
+            f"#{tab_id_prefix}-variables-tab.nav-link",
+            data_bs_toggle="tab",
+            data_bs_target=f"#{tab_id_prefix}-variables-pane",
+            type="button",
+            role="tab",
+            aria_controls=f"{tab_id_prefix}-variables-pane",
+            aria_selected="false",
+        )["Variables"]
+    ]


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

Reply via email to