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>

Reply via email to