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>
77 lines
2.0 KiB
Python
77 lines
2.0 KiB
Python
"""Participant business logic utilities."""
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from src.models import Participant
|
|
|
|
|
|
def get_active_participants(exchange_id: int) -> list["Participant"]:
|
|
"""Get all active (non-withdrawn) participants for an exchange.
|
|
|
|
Args:
|
|
exchange_id: ID of the exchange
|
|
|
|
Returns:
|
|
List of active participants, ordered by name
|
|
"""
|
|
from src.models.participant import Participant
|
|
|
|
result: list[Participant] = (
|
|
Participant.query.filter(
|
|
Participant.exchange_id == exchange_id, Participant.withdrawn_at.is_(None)
|
|
)
|
|
.order_by(Participant.name)
|
|
.all()
|
|
)
|
|
return result
|
|
|
|
|
|
def is_withdrawn(participant: "Participant") -> bool:
|
|
"""Check if participant has withdrawn.
|
|
|
|
Args:
|
|
participant: The participant to check
|
|
|
|
Returns:
|
|
True if withdrawn, False otherwise
|
|
"""
|
|
return participant.withdrawn_at is not None
|
|
|
|
|
|
def can_update_profile(participant: "Participant") -> bool:
|
|
"""Check if participant can update their profile.
|
|
|
|
Profile updates are allowed until matching occurs.
|
|
|
|
Args:
|
|
participant: The participant to check
|
|
|
|
Returns:
|
|
True if profile updates are allowed, False otherwise
|
|
"""
|
|
exchange = participant.exchange
|
|
allowed_states = ["draft", "registration_open", "registration_closed"]
|
|
return exchange.state in allowed_states
|
|
|
|
|
|
def can_withdraw(participant: "Participant") -> bool:
|
|
"""Check if participant can withdraw from the exchange.
|
|
|
|
Withdrawals are only allowed before registration closes.
|
|
After that, admin intervention is required.
|
|
|
|
Args:
|
|
participant: The participant to check
|
|
|
|
Returns:
|
|
True if withdrawal is allowed, False otherwise
|
|
"""
|
|
# Already withdrawn
|
|
if participant.withdrawn_at is not None:
|
|
return False
|
|
|
|
exchange = participant.exchange
|
|
allowed_states = ["draft", "registration_open"]
|
|
return exchange.state in allowed_states
|