feat: implement reminder preferences and withdrawal (Stories 6.3, 6.2)

Implement Phase 3 participant self-management features:

Story 6.3 - Reminder Preferences:
- Add ReminderPreferenceForm to participant forms
- Add update_preferences route for preference updates
- Update dashboard template with reminder preference toggle
- Participants can enable/disable reminder emails at any time

Story 6.2 - Withdrawal from Exchange:
- Add can_withdraw utility function for state validation
- Create WithdrawalService to handle withdrawal process
- Add WithdrawForm with explicit confirmation requirement
- Add withdraw route with GET (confirmation) and POST (process)
- Add withdrawal confirmation email template
- Update dashboard to show withdraw link when allowed
- Withdrawal only allowed before registration closes
- Session cleared after withdrawal, user redirected to registration

All acceptance criteria met for both stories.
Test coverage: 90.02% (156 tests passing)
Linting and type checking: passed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-22 21:18:18 -07:00
parent 4fbb681e03
commit c2b3641d74
12 changed files with 725 additions and 2 deletions

View File

@@ -0,0 +1,83 @@
"""Integration tests for reminder preferences."""
from flask import url_for
def get_csrf_token(client, url):
"""Extract CSRF token from a page.
Args:
client: Flask test client.
url: URL to fetch the CSRF token from.
Returns:
CSRF token string.
"""
response = client.get(url)
# Extract CSRF token from the form
data = response.data.decode()
start = data.find('name="csrf_token" value="') + len('name="csrf_token" value="')
end = data.find('"', start)
return data[start:end]
def test_dashboard_shows_reminder_preference_form(client, auth_participant):
"""Test that dashboard shows reminder preference form."""
response = client.get(
url_for("participant.dashboard", id=auth_participant.exchange_id)
)
assert response.status_code == 200
assert b"Email Reminders" in response.data or b"Send me reminders" in response.data
def test_update_preferences_enable(client, auth_participant, db):
"""Enable reminder emails."""
auth_participant.reminder_enabled = False
db.session.commit()
csrf_token = get_csrf_token(
client, url_for("participant.dashboard", id=auth_participant.exchange_id)
)
response = client.post(
url_for("participant.update_preferences"),
data={"reminder_enabled": True, "csrf_token": csrf_token},
follow_redirects=True,
)
assert response.status_code == 200
assert b"Reminder emails enabled" in response.data
db.session.refresh(auth_participant)
assert auth_participant.reminder_enabled is True
def test_update_preferences_disable(client, auth_participant, db):
"""Disable reminder emails."""
auth_participant.reminder_enabled = True
db.session.commit()
csrf_token = get_csrf_token(
client, url_for("participant.dashboard", id=auth_participant.exchange_id)
)
response = client.post(
url_for("participant.update_preferences"),
data={"csrf_token": csrf_token},
follow_redirects=True,
)
assert response.status_code == 200
assert b"Reminder emails disabled" in response.data
db.session.refresh(auth_participant)
assert auth_participant.reminder_enabled is False
def test_update_preferences_requires_login(client):
"""Test that update_preferences requires login."""
response = client.post(url_for("participant.update_preferences"))
# Should redirect to login or show error
assert response.status_code in [302, 401, 403]