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

commit 7e12b2bf3a4c3177f1e4dac6dafd775a833611b0
Author: Greg Stein <[email protected]>
AuthorDate: Fri Feb 20 00:26:12 2026 -0600

    refactor: add helper function and refactor date-setting endpoints to reduce 
duplication
    
    Co-authored-by: aider (openrouter/x-ai/grok-code-fast-1) <[email protected]>
---
 v3/TODO.md         | 60 +++-------------------------------------------
 v3/server/pages.py | 70 ++++++++++++++++++++++--------------------------------
 2 files changed, 32 insertions(+), 98 deletions(-)

diff --git a/v3/TODO.md b/v3/TODO.md
index 01e3f3c..166611f 100644
--- a/v3/TODO.md
+++ b/v3/TODO.md
@@ -2,64 +2,10 @@
 
 Based on a review of `v3/server/pages.py` (and related templates like 
`voter.ezt`), here are the identified issues, errors, and areas for 
improvement. These focus on correctness, logic, security, and completeness, 
while adhering to project conventions (minimal changes, no unsolicited 
refactors). Prioritized by impact.
 
-## 1. Missing Endpoints for Date Saving (Critical)
-- **Issue**: The `manage.ezt` template includes JavaScript that makes POST 
requests to `/do-set-open_at/<eid>` and `/do-set-close_at/<eid>` for 
auto-saving open/close dates. These endpoints are not defined in `pages.py`, 
causing the auto-save functionality to fail (likely with 404 errors).
+## 1. Missing Endpoints for Date Saving (Critical) - RESOLVED
+- **Issue**: The `manage.ezt` template includes JavaScript that makes POST 
requests to `/do-set-open_at/<eid>` and `/do-set-close_at/<eid>` for 
auto-saving open/close dates. These endpoints were not defined in `pages.py`, 
causing the auto-save functionality to fail (likely with 404 errors).
 - **Impact**: Users won't be able to save dates via the UI, breaking the 
intended workflow.
-- **Suggested Fix**: Add these two endpoints. They should:
-  - Require authentication (e.g., `@asfquart.auth.require({R.committer})`).
-  - Use the `@load_election` decorator to load the election.
-  - Parse the JSON body for a 'date' field (expecting a string like 
'YYYY-MM-DD').
-  - Validate the date (e.g., ensure it's a valid date string and in the future 
for open_at/close_at).
-  - Update the election's `open_at` or `close_at` via the Election class 
(assuming it has methods like `set_open_at(date)` or direct attribute setting).
-  - Handle CSRF (the JS sends 'X-CSRFToken' header; verify it matches the 
session's token).
-  - Return 204 on success (no content, as it's auto-save). On failure, return 
400/500 with an error message.
-  - Log the action (e.g., `_LOGGER.info(f'User[U:{result.uid}] updated {field} 
for election[E:{election.eid}]')`).
-  - No flash messages needed for auto-save, but consider error handling in the 
JS (already partially done).
-- **Example Implementation Sketch** (add near the other `/do-*` endpoints; 
keep it minimal):
-  ```python
-  @APP.post('/do-set-open_at/<eid>')
-  @asfquart.auth.require({R.committer})
-  @load_election
-  async def do_set_open_at_endpoint(election):
-      result = await basic_info()
-      # TODO: Check authz if needed
-      data = await quart.request.get_json()
-      date_str = data.get('date')
-      if not date_str:
-          quart.abort(400, 'Missing date')
-      # Validate date (basic check)
-      try:
-          dt = datetime.datetime.fromisoformat(date_str).date()
-      except ValueError:
-          quart.abort(400, 'Invalid date format')
-      # Assume Election has a method to set it
-      election.set_open_at(dt)  # Or direct: election.open_at = dt.timestamp()
-      _LOGGER.info(f'User[U:{result.uid}] set open_at for 
election[E:{election.eid}] to {date_str}')
-      return '', 204
-
-  @APP.post('/do-set-close_at/<eid>')
-  @asfquart.auth.require({R.committer})
-  @load_election
-  async def do_set_close_at_endpoint(election):
-      # Similar to above, but for close_at
-      result = await basic_info()
-      # TODO: Check authz
-      data = await quart.request.get_json()
-      date_str = data.get('date')
-      if not date_str:
-          quart.abort(400, 'Missing date')
-      try:
-          dt = datetime.datetime.fromisoformat(date_str).date()
-      except ValueError:
-          quart.abort(400, 'Invalid date format')
-      election.set_close_at(dt)
-      _LOGGER.info(f'User[U:{result.uid}] set close_at for 
election[E:{election.eid}] to {date_str}')
-      return '', 204
-  ```
-- **Notes**: 
-  - Confirm if the Election class supports setting dates (check 
`steve.election.Election`). If not, you may need to add methods there.
-  - For editable elections, ensure open_at/close_at can be set; for open 
elections, only close_at.
-  - Test CSRF: The `basic_info()` sets `csrf_token = 'placeholder'`, so 
implement real CSRF generation/verification (e.g., via a library or session) 
before deploying.
+- **Resolution**: Added the two endpoints with a refactored helper function 
`_set_election_date` to handle common logic (auth, JSON parsing, validation, 
setting dates, logging, and response). Endpoints now require authentication, 
validate dates, and log actions. CSRF handling remains a TODO (placeholder 
token in use). Test for proper date-setting and error handling.
 
 ## 2. Upcoming Elections Not Populated in `voter_page()`
 - **Issue**: The `voter.ezt` template checks for `[if-any upcoming]` and loops 
over `upcoming` elections, but `voter_page()` only sets `result.election` (for 
open elections). `result.upcoming` is never defined, so the "Upcoming 
Elections" section will always be empty.
diff --git a/v3/server/pages.py b/v3/server/pages.py
index 437bbbc..8c9daf9 100644
--- a/v3/server/pages.py
+++ b/v3/server/pages.py
@@ -90,6 +90,33 @@ async def basic_info():
     return basic
 
 
+async def _set_election_date(election, field):
+    """Helper to set open_at or close_at on an election, with validation and 
logging."""
+    result = await basic_info()
+    ### check authz
+    data = await quart.request.get_json()
+    date_str = data.get('date')
+    if not date_str:
+        quart.abort(400, 'Missing date')
+    
+    # Validate date (basic check)
+    try:
+        dt = datetime.datetime.fromisoformat(date_str).date()
+    except ValueError:
+        quart.abort(400, 'Invalid date format')
+    
+    # Set the date on the election (field is 'open_at' or 'close_at')
+    if field == 'open_at':
+        election.set_open_at(dt)
+    elif field == 'close_at':
+        election.set_close_at(dt)
+    else:
+        quart.abort(400, 'Invalid field')
+    
+    _LOGGER.info(f'User[U:{result.uid}] set {field} for 
election[E:{election.eid}] to {date_str}')
+    return '', 204
+
+
 # Define a bunch of helpers for recording "flash" messages in the session.
 # Each helper function is:
 #    async def flash_FOO(message)
@@ -317,53 +344,14 @@ async def manage_stv_page(election, issue):
 @asfquart.auth.require({R.committer})
 @load_election
 async def do_set_open_at_endpoint(election):
-    result = await basic_info()
-
-    ### check authz
-
-    data = await quart.request.get_json()
-    date_str = data.get('date')
-    if not date_str:
-        quart.abort(400, 'Missing date')
-
-    # Validate date (basic check)
-    try:
-        dt = datetime.datetime.fromisoformat(date_str).date()
-    except ValueError:
-        quart.abort(400, 'Invalid date format')
-
-    # Record the opening date.
-    election.set_open_at(dt)
-
-    _LOGGER.info(f'User[U:{result.uid}] set open_at for 
election[E:{election.eid}] to {date_str}')
-    return '', 204
+    return await _set_election_date(election, 'open_at')
 
 
 @APP.post('/do-set-close_at/<eid>')
 @asfquart.auth.require({R.committer})
 @load_election
 async def do_set_close_at_endpoint(election):
-    # Similar to above, but for close_at
-    result = await basic_info()
-
-    ### check authz
-
-    data = await quart.request.get_json()
-    date_str = data.get('date')
-    if not date_str:
-        quart.abort(400, 'Missing date')
-
-    # Validate date (basic check)
-    try:
-        dt = datetime.datetime.fromisoformat(date_str).date()
-    except ValueError:
-        quart.abort(400, 'Invalid date format')
-
-    # Record the closing date.
-    election.set_close_at(dt)
-
-    _LOGGER.info(f'User[U:{result.uid}] set close_at for 
election[E:{election.eid}] to {date_str}')
-    return '', 204
+    return await _set_election_date(election, 'close_at')
 
 
 @APP.post('/do-create-election')

Reply via email to