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]
