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

commit 356fb5192b2eedc795a7c3b8f64a69b7fda6e835
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Dec 10 15:38:54 2025 +0000

    Migrate keys, projects, and vote forms to standalone files
---
 atr/get/announce.py                |  2 +-
 atr/get/distribution.py            |  6 +--
 atr/get/keys.py                    | 31 +------------
 atr/get/projects.py                | 29 +++++-------
 atr/get/tokens.py                  |  5 +-
 atr/get/voting.py                  | 94 ++++----------------------------------
 atr/shared/__init__.py             |  7 +--
 atr/static/js/announce-preview.js  | 18 --------
 atr/static/js/copy-variable.js     | 18 ++++++++
 atr/static/js/keys-add-toggle.js   | 22 +++++++++
 atr/static/js/projects-add-form.js | 14 ++++++
 atr/static/js/vote-preview.js      | 70 ++++++++++++++++++++++++++++
 12 files changed, 153 insertions(+), 163 deletions(-)

diff --git a/atr/get/announce.py b/atr/get/announce.py
index fddc8e5..cda0930 100644
--- a/atr/get/announce.py
+++ b/atr/get/announce.py
@@ -82,7 +82,7 @@ async def selected(session: web.Committer, project_name: str, 
version_name: str)
         title=f"Announce and distribute {release.project.display_name} 
{release.version}",
         description=f"Announce and distribute {release.project.display_name} 
{release.version} as a release.",
         content=content,
-        javascripts=["announce-preview"],
+        javascripts=["announce-preview", "copy-variable"],
     )
 
 
diff --git a/atr/get/distribution.py b/atr/get/distribution.py
index 8e12cd4..28985fe 100644
--- a/atr/get/distribution.py
+++ b/atr/get/distribution.py
@@ -100,11 +100,7 @@ async def list_get(session: web.Committer, project: str, 
version: str) -> str:
                 "package": dist.package,
                 "version": dist.version,
             },
-            confirm=(
-                f"Are you sure you want to delete the distribution "
-                f"{dist.platform.name} {dist.package} {dist.version}? "
-                f"This cannot be undone."
-            ),
+            confirm=("Are you sure you want to delete this distribution? This 
cannot be undone."),
         )
         block.append(htm.div(".mb-3")[delete_form])
 
diff --git a/atr/get/keys.py b/atr/get/keys.py
index d969200..9951ee8 100644
--- a/atr/get/keys.py
+++ b/atr/get/keys.py
@@ -19,7 +19,6 @@
 import datetime
 
 import htpy
-import markupsafe
 import quart
 
 import atr.blueprints.get as get
@@ -61,39 +60,11 @@ async def add(session: web.Committer) -> str:
         },
     )
 
-    page.append(
-        htm.script[
-            markupsafe.Markup("""
-            document.addEventListener("DOMContentLoaded", function() {
-                const checkboxes = 
document.querySelectorAll("input[name='selected_committees']");
-                if (checkboxes.length === 0) return;
-
-                const firstCheckbox = checkboxes[0];
-                const container = firstCheckbox.closest(".col-sm-8");
-                if (!container) return;
-
-                const button = document.createElement("button");
-                button.id = "toggleCommitteesBtn";
-                button.type = "button";
-                button.className = "btn btn-outline-secondary btn-sm mt-2";
-                button.textContent = "Select all committees";
-
-                button.addEventListener("click", function() {
-                    const allChecked = Array.from(checkboxes).every(cb => 
cb.checked);
-                    checkboxes.forEach(cb => cb.checked = !allChecked);
-                    button.textContent = allChecked ? "Select all committees" 
: "Deselect all committees";
-                });
-
-                container.appendChild(button);
-            });
-            """)
-        ]
-    )
-
     return await template.blank(
         "Add your OpenPGP key",
         content=page.collect(),
         description="Add your public signing key to your ATR account.",
+        javascripts=["keys-add-toggle"],
     )
 
 
diff --git a/atr/get/projects.py b/atr/get/projects.py
index 307d5c7..f896b75 100644
--- a/atr/get/projects.py
+++ b/atr/get/projects.py
@@ -19,7 +19,6 @@ from __future__ import annotations
 
 import asfquart.base as base
 import htpy
-import markupsafe
 
 import atr.blueprints.get as get
 import atr.config as config
@@ -34,7 +33,6 @@ import atr.htm as htm
 import atr.models.sql as sql
 import atr.post as post
 import atr.registry as registry
-import atr.render as render
 import atr.shared as shared
 import atr.template as template
 import atr.user as user
@@ -69,26 +67,20 @@ async def add_project(session: web.Committer, 
committee_name: str) -> web.Werkze
         },
     )
 
-    script_text = markupsafe.Markup(f"""
-document.addEventListener("DOMContentLoaded", function() {{
-    const committeeInput = 
document.querySelector('input[name="committee_name"]');
-    if (committeeInput) {{
-        const committeeName = committeeInput.value;
-        const committeeDisplayName = "{committee_display_name}";
-        const formTexts = document.querySelectorAll('.form-text, .text-muted');
-        formTexts.forEach(function(element) {{
-            element.textContent = element.textContent.replace(/Example/g, 
committeeDisplayName);
-            element.textContent = element.textContent.replace(/example/g, 
committeeName.toLowerCase());
-        }});
-    }}
-}});
-""")
-    page.append(htm.script[script_text])
+    # TODO: It would be better to have these attributes on the form
+    page.append(
+        htpy.div(
+            "#projects-add-config.d-none",
+            data_committee_name=committee_name,
+            data_committee_display_name=committee_display_name,
+        )
+    )
 
     return await template.blank(
         title="Add project",
         description=f"Add a new project to the {committee.display_name} 
committee.",
         content=page.collect(),
+        javascripts=["projects-add-form"],
     )
 
 
@@ -214,10 +206,12 @@ async def view(session: web.Committer, name: str) -> 
web.WerkzeugResponse | str:
 
     content = page.collect()
 
+    javascripts = ["copy-variable"] if can_edit else []
     return await template.blank(
         title=f"{project.display_name}",
         description=f"Information regarding {project.display_name}.",
         content=content,
+        javascripts=javascripts,
     )
 
 
@@ -562,7 +556,6 @@ def _render_vote_form(project: sql.Project) -> htm.Element:
             skip=skip_fields,
             custom={"release_checklist": release_checklist_widget},
         )
-        card_body.append(htm.script[render.copy_javascript()])
     return card.collect()
 
 
diff --git a/atr/get/tokens.py b/atr/get/tokens.py
index 10f6c19..f9a11af 100644
--- a/atr/get/tokens.py
+++ b/atr/get/tokens.py
@@ -78,12 +78,11 @@ async def tokens(session: web.Committer) -> str:
         ]
     page.append(jwt_section)
 
-    return await template.render_sync(
-        "blank.html",
+    return await template.blank(
         title="Tokens",
         description="Manage your PATs and JWTs.",
         content=page.collect(),
-        javascripts=[util.static_path("js", "create-a-jwt.js")],
+        javascripts=["create-a-jwt"],
     )
 
 
diff --git a/atr/get/voting.py b/atr/get/voting.py
index 0fff05b..6c095d3 100644
--- a/atr/get/voting.py
+++ b/atr/get/voting.py
@@ -18,7 +18,6 @@
 
 import aiofiles.os
 import htpy
-import markupsafe
 
 import atr.blueprints.get as get
 import atr.construct as construct
@@ -86,7 +85,9 @@ async def selected_revision(
         )
 
         return await template.blank(
-            title=f"Start voting on {release.project.short_display_name} 
{release.version}", content=content
+            title=f"Start voting on {release.project.short_display_name} 
{release.version}",
+            content=content,
+            javascripts=["copy-variable", "vote-preview"],
         )
 
 
@@ -167,93 +168,16 @@ async def _render_page(
         },
     )
     page.append(vote_form)
-    page.append(_render_javascript(release, min_hours))
 
-    return page.collect()
-
-
-def _render_body_tabs(default_body: str) -> htm.Element:
-    """Render the tabbed interface for body editing and preview."""
-    return render.body_tabs("vote-body", default_body, 
construct.vote_template_variables())
-
-
-def _render_javascript(release, min_hours: int) -> htm.Element:
-    """Render the JavaScript for email preview."""
     preview_url = util.as_url(
         post.preview.vote_preview, project_name=release.project.name, 
version_name=release.version
     )
+    # TODO: It would be better to have these attributes on the form
+    page.append(htpy.div("#vote-config.d-none", data_preview_url=preview_url, 
data_min_hours=str(min_hours)))
 
-    js_code = f"""
-        document.addEventListener("DOMContentLoaded", () => {{
-            let debounceTimeout;
-            const debounceDelay = 500;
-
-            const bodyTextarea = document.getElementById("body");
-            const voteDurationInput = document.getElementById("vote_duration");
-            const textPreviewContent = 
document.getElementById("vote-body-preview-content");
-            const voteForm = document.querySelector("form.atr-canary");
-
-            if (!bodyTextarea || !voteDurationInput || !textPreviewContent || 
!voteForm) {{
-                console.error("Required elements for vote preview not found. 
Exiting.");
-                return;
-            }}
-
-            const previewUrl = "{preview_url}";
-            const csrfTokenInput = 
voteForm.querySelector('input[name="csrf_token"]');
-
-            if (!previewUrl || !csrfTokenInput) {{
-                console.error("Required data attributes or CSRF token not 
found for vote preview.");
-                return;
-            }}
-            const csrfToken = csrfTokenInput.value;
-
-            function fetchAndUpdateVotePreview() {{
-                const bodyContent = bodyTextarea.value;
-                const voteDuration = voteDurationInput.value || "{min_hours}";
-
-                fetch(previewUrl, {{
-                        method: "POST",
-                        headers: {{
-                            "Content-Type": 
"application/x-www-form-urlencoded",
-                            "X-CSRFToken": csrfToken
-                        }},
-                        body: new URLSearchParams({{
-                            "body": bodyContent,
-                            "duration": voteDuration,
-                            "csrf_token": csrfToken
-                        }})
-                    }})
-                    .then(response => {{
-                        if (!response.ok) {{
-                            return response.text().then(text => {{
-                                throw new Error(`HTTP error 
${{response.status}}: ${{text}}`)
-                            }});
-                        }}
-                        return response.text();
-                    }})
-                    .then(previewText => {{
-                        textPreviewContent.textContent = previewText;
-                    }})
-                    .catch(error => {{
-                        console.error("Error fetching email preview:", error);
-                        textPreviewContent.textContent = `Error loading 
preview:\\n${{error.message}}`;
-                    }});
-            }}
-
-            bodyTextarea.addEventListener("input", () => {{
-                clearTimeout(debounceTimeout);
-                debounceTimeout = setTimeout(fetchAndUpdateVotePreview, 
debounceDelay);
-            }});
-
-            voteDurationInput.addEventListener("input", () => {{
-                clearTimeout(debounceTimeout);
-                debounceTimeout = setTimeout(fetchAndUpdateVotePreview, 
debounceDelay);
-            }});
-
-            fetchAndUpdateVotePreview();
+    return page.collect()
 
-            {render.copy_javascript()}
-        }});
-    """
 
-    return htpy.script[markupsafe.Markup(js_code)]
+def _render_body_tabs(default_body: str) -> htm.Element:
+    """Render the tabbed interface for body editing and preview."""
+    return render.body_tabs("vote-body", default_body, 
construct.vote_template_variables())
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index ded07fa..b7d6192 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -145,10 +145,11 @@ async def check(
             submit_label="Delete",
             empty=True,
             defaults={"file_path": str(path)},
+            # TODO: Add a static check for the confirm syntax
             confirm=(
-                f"Are you sure you want to delete {path}? "
-                f"This will also delete any associated metadata files. "
-                f"This cannot be undone."
+                "Are you sure you want to delete this file? "
+                "This will also delete any associated metadata files. "
+                "This cannot be undone."
             ),
         )
 
diff --git a/atr/static/js/announce-preview.js 
b/atr/static/js/announce-preview.js
index 691ef95..1833864 100644
--- a/atr/static/js/announce-preview.js
+++ b/atr/static/js/announce-preview.js
@@ -59,24 +59,6 @@ document.addEventListener("DOMContentLoaded", () => {
 
     fetchAndUpdateAnnouncePreview();
 
-    // Copy variable button functionality
-    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);
-            });
-        });
-    });
-
     // Download path suffix validation
     const pathInput = document.getElementById("download_path_suffix");
     const pathHelpText = pathInput ? 
pathInput.parentElement.querySelector(".form-text") : null;
diff --git a/atr/static/js/copy-variable.js b/atr/static/js/copy-variable.js
new file mode 100644
index 0000000..2b81d51
--- /dev/null
+++ b/atr/static/js/copy-variable.js
@@ -0,0 +1,18 @@
+document.addEventListener("DOMContentLoaded", function () {
+    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);
+            });
+        });
+    });
+});
diff --git a/atr/static/js/keys-add-toggle.js b/atr/static/js/keys-add-toggle.js
new file mode 100644
index 0000000..8ac378d
--- /dev/null
+++ b/atr/static/js/keys-add-toggle.js
@@ -0,0 +1,22 @@
+document.addEventListener("DOMContentLoaded", function() {
+    const checkboxes = 
document.querySelectorAll("input[name='selected_committees']");
+    if (checkboxes.length === 0) return;
+
+    const firstCheckbox = checkboxes[0];
+    const container = firstCheckbox.closest(".col-sm-8");
+    if (!container) return;
+
+    const button = document.createElement("button");
+    button.id = "toggleCommitteesBtn";
+    button.type = "button";
+    button.className = "btn btn-outline-secondary btn-sm mt-2";
+    button.textContent = "Select all committees";
+
+    button.addEventListener("click", function() {
+        const allChecked = Array.from(checkboxes).every(cb => cb.checked);
+        checkboxes.forEach(cb => cb.checked = !allChecked);
+        button.textContent = allChecked ? "Select all committees" : "Deselect 
all committees";
+    });
+
+    container.appendChild(button);
+});
diff --git a/atr/static/js/projects-add-form.js 
b/atr/static/js/projects-add-form.js
new file mode 100644
index 0000000..aff9e77
--- /dev/null
+++ b/atr/static/js/projects-add-form.js
@@ -0,0 +1,14 @@
+document.addEventListener("DOMContentLoaded", function() {
+    const configElement = document.getElementById("projects-add-config");
+    if (!configElement) return;
+
+    const committeeDisplayName = configElement.dataset.committeeDisplayName;
+    const committeeName = configElement.dataset.committeeName;
+    if (!committeeDisplayName || !committeeName) return;
+
+    const formTexts = document.querySelectorAll(".form-text, .text-muted");
+    formTexts.forEach(function(element) {
+        element.textContent = element.textContent.replace(/Example/g, 
committeeDisplayName);
+        element.textContent = element.textContent.replace(/example/g, 
committeeName.toLowerCase());
+    });
+});
diff --git a/atr/static/js/vote-preview.js b/atr/static/js/vote-preview.js
new file mode 100644
index 0000000..270fe71
--- /dev/null
+++ b/atr/static/js/vote-preview.js
@@ -0,0 +1,70 @@
+document.addEventListener("DOMContentLoaded", () => {
+    let debounceTimeout;
+    const debounceDelay = 500;
+
+    const bodyTextarea = document.getElementById("body");
+    const voteDurationInput = document.getElementById("vote_duration");
+    const textPreviewContent = 
document.getElementById("vote-body-preview-content");
+    const voteForm = document.querySelector("form.atr-canary");
+    const configElement = document.getElementById("vote-config");
+
+    if (!bodyTextarea || !voteDurationInput || !textPreviewContent || 
!voteForm) {
+        console.error("Required elements for vote preview not found. 
Exiting.");
+        return;
+    }
+
+    const previewUrl = configElement ? configElement.dataset.previewUrl : null;
+    const minHours = configElement ? configElement.dataset.minHours : "72";
+    const csrfTokenInput = voteForm.querySelector('input[name="csrf_token"]');
+
+    if (!previewUrl || !csrfTokenInput) {
+        console.error("Required data attributes or CSRF token not found for 
vote preview.");
+        return;
+    }
+    const csrfToken = csrfTokenInput.value;
+
+    function fetchAndUpdateVotePreview() {
+        const bodyContent = bodyTextarea.value;
+        const voteDuration = voteDurationInput.value || minHours;
+
+        fetch(previewUrl, {
+                method: "POST",
+                headers: {
+                    "Content-Type": "application/x-www-form-urlencoded",
+                    "X-CSRFToken": csrfToken
+                },
+                body: new URLSearchParams({
+                    "body": bodyContent,
+                    "duration": voteDuration,
+                    "csrf_token": csrfToken
+                })
+            })
+            .then(response => {
+                if (!response.ok) {
+                    return response.text().then(text => {
+                        throw new Error(`HTTP error ${response.status}: 
${text}`)
+                    });
+                }
+                return response.text();
+            })
+            .then(previewText => {
+                textPreviewContent.textContent = previewText;
+            })
+            .catch(error => {
+                console.error("Error fetching email preview:", error);
+                textPreviewContent.textContent = `Error loading 
preview:\n${error.message}`;
+            });
+    }
+
+    bodyTextarea.addEventListener("input", () => {
+        clearTimeout(debounceTimeout);
+        debounceTimeout = setTimeout(fetchAndUpdateVotePreview, debounceDelay);
+    });
+
+    voteDurationInput.addEventListener("input", () => {
+        clearTimeout(debounceTimeout);
+        debounceTimeout = setTimeout(fetchAndUpdateVotePreview, debounceDelay);
+    });
+
+    fetchAndUpdateVotePreview();
+});


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

Reply via email to