This is an automated email from the ASF dual-hosted git repository.
gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git
The following commit(s) were added to refs/heads/trunk by this push:
new 9de06cd Focus on form submission via POST.
9de06cd is described below
commit 9de06cd8816a8ed549dae01fbcb4145c069f66e4
Author: Greg Stein <[email protected]>
AuthorDate: Sun Oct 19 21:07:59 2025 -0500
Focus on form submission via POST.
The use of fetch() to send DELETE created a pit of despair. The
fetch() function does not properly follow 303 redirects after the
operation completes. That led to trying alternatives, and then to GET
as a state-altering invocation (yes, bad, but trying to get 303 to
work). Meanwhile, trying to insert a confirm() call on these state-
altering features.
Answer: switch to a form, rather than confirm(). This allows using
Bootstrap for better UX alignment, and that form will send a POST
where the browser understands the 303 response and properly follows to
the desired Location.
* use a modal for confirming delete of an issue, not a confirm()
* for add/edit of an issue, switch to a form POST, not a fetch()
note: small TBD tweak: add csrf_token to basic template data
---
v3/server/pages.py | 5 ++-
v3/server/templates/manage.ezt | 84 +++++++++++++++++++++++-------------------
2 files changed, 50 insertions(+), 39 deletions(-)
diff --git a/v3/server/pages.py b/v3/server/pages.py
index cd83822..c406cb7 100644
--- a/v3/server/pages.py
+++ b/v3/server/pages.py
@@ -77,6 +77,9 @@ async def basic_info():
# No session.
basic.update(uid=None, name=None, email=None,)
+ ### generate a real token and store in the session
+ basic.csrf_token = 'placeholder'
+
return basic
@@ -360,7 +363,7 @@ async def do_edit_issue_endpoint(election, issue):
return quart.redirect(f'/manage/{election.eid}', code=303)
[email protected]('/do-delete-issue/<eid>/<iid>')
[email protected]('/do-delete-issue/<eid>/<iid>')
@asfquart.auth.require({R.committer}) ### need general solution
@load_election_issue
async def do_delete_issue_endpoint(election, issue):
diff --git a/v3/server/templates/manage.ezt b/v3/server/templates/manage.ezt
index 9a1cd6f..985e7ab 100644
--- a/v3/server/templates/manage.ezt
+++ b/v3/server/templates/manage.ezt
@@ -72,16 +72,16 @@
<button type="button" class="btn-close"
data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
- <form id="issueForm">
- <input type="hidden" id="issueId" value="">
+ <form id="issueForm" method="POST" action="">
+ <input type="hidden" id="issueId" name="issueId"
value="">
<div class="mb-3">
<label for="issueTitle"
class="form-label">Title</label>
- <input type="text" class="form-control"
id="issueTitle" required>
+ <input type="text" class="form-control"
id="issueTitle" name="title" required>
<div class="invalid-feedback">Title is
required.</div>
</div>
<div class="mb-3">
<label for="issueDescription"
class="form-label">Description</label>
- <textarea class="form-control"
id="issueDescription" rows="4"></textarea>
+ <textarea class="form-control"
id="issueDescription" name="description" rows="4"></textarea>
</div>
</form>
</div>
@@ -93,6 +93,29 @@
</div>[# modal-dialog ]
</div>[# id=issueModal ]
+ <!-- Delete Issue Confirmation Modal -->
+ <div class="modal fade" id="deleteIssueModal" tabindex="-1"
aria-labelledby="deleteIssueModalLabel" aria-hidden="true">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title" id="deleteIssueModalLabel">Confirm
Delete</h5>
+ <button type="button" class="btn-close"
data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p id="deleteIssueMessage">Are you sure you want to delete
this issue?</p>
+ <form id="deleteIssueForm" method="POST" action="">
+ <input type="hidden" name="csrf_token"
value="[csrf_token]">
+ <input type="hidden" id="deleteIssueId" name="issueId"
value="">
+ </form>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Cancel</button>
+ <button type="submit" class="btn btn-danger"
form="deleteIssueForm">Confirm</button>
+ </div>
+ </div>[# modal-content ]
+ </div>[# modal-dialog ]
+ </div>[# id=deleteIssueModal ]
+
<div>
State: [e_state]
</div>
@@ -127,8 +150,9 @@
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-outline-danger
btn-sm"
- id="delete-[issues.iid]"
- onclick="deleteIssue('[issues.iid]')"
aria-label="Delete Issue">
+ onclick="openDeleteIssueModal('[issues.iid]',
+ '[format
"js,html"][issues.title][end]')"
+ aria-label="Delete Issue">
<i class="bi bi-trash"></i>
</button>
</div>
@@ -181,7 +205,6 @@
}
}
-
// Expand/Collapse All descriptions
let allExpanded = false;
function toggleAllDescriptions() {
@@ -198,6 +221,8 @@
// Open modal for adding issue
function openAddIssueModal() {
+ const form = document.getElementById('issueForm');
+ form.action = '/do-add-issue/[eid]';
document.getElementById('issueModalLabel').textContent = 'Add Issue';
document.getElementById('issueId').value = '';
document.getElementById('issueTitle').value = '';
@@ -209,6 +234,8 @@
// Open modal for editing issue
function openEditIssueModal(issueId, title, description) {
+ const form = document.getElementById('issueForm');
+ form.action = `/do-edit-issue/[eid]/${issueId}`;
document.getElementById('issueModalLabel').textContent = 'Edit Issue';
document.getElementById('issueId').value = issueId;
document.getElementById('issueTitle').value = title;
@@ -218,11 +245,19 @@
modal.show();
}
+ // Open modal for deleting issue
+ function openDeleteIssueModal(issueId, title) {
+ const form = document.getElementById('deleteIssueForm');
+ form.action = `/do-delete-issue/[eid]/${issueId}`;
+ document.getElementById('deleteIssueId').value = issueId;
+ document.getElementById('deleteIssueMessage').textContent = `Are you
sure you want to delete "${title}"?`;
+ const modal = new
bootstrap.Modal(document.getElementById('deleteIssueModal'));
+ modal.show();
+ }
+
// Save issue (add or edit)
function saveIssue() {
- const issueId = document.getElementById('issueId').value;
const title = document.getElementById('issueTitle').value.trim();
- const description =
document.getElementById('issueDescription').value.trim();
const titleInput = document.getElementById('issueTitle');
// Client-side validation
@@ -232,35 +267,8 @@
}
titleInput.classList.remove('is-invalid');
- const url = issueId ? `/do-edit-issue/[eid]/${issueId}` :
`/do-add-issue/[eid]`;
- fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ title, description }),
- })
- .then(response => {
- if (response.status === 303) {
- const redirectUrl = response.headers.get('Location');
- window.location.href = redirectUrl; // Navigate to server’s
Location
- } else {
- // Fallback for any non-303 response (success or error)
- window.location.reload(); // Let server render flash messages
or error page
- }
- })
- .catch(error => {
- console.error('Network error:', error);
- window.location.reload(); // Reload to show server’s error page or
flash message
- });
- }
-
- // Delete issue
- function deleteIssue(issueId) {
- if (!confirm('Are you sure you want to delete this issue?')) return;
-
- const button = document.querySelector(`#delete-${issueId}`);
- button.disabled = true; // Prevent multiple clicks
-
- window.location.href = `/do-delete-issue/[eid]/${issueId}`;
+ // Submit the form
+ document.getElementById('issueForm').submit();
}
</script>