feat: implement Story 4.3 - Returning Participant Detection
Prevent duplicate registrations for the same exchange: - Check for existing participant with same email (case-insensitive) - Show friendly message if already registered - Offer link to request new magic link for access - Allow same email to register for different exchanges - Comprehensive test suite with 4 tests All tests passing (90 total), 91% coverage maintained. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,26 @@ def register(slug: str):
|
|||||||
# If explicitly unchecked, it will be False
|
# If explicitly unchecked, it will be False
|
||||||
reminder_enabled = form.reminder_enabled.data
|
reminder_enabled = form.reminder_enabled.data
|
||||||
|
|
||||||
|
# Check if participant with this email already exists for this exchange
|
||||||
|
existing_participant = (
|
||||||
|
db.session.query(Participant)
|
||||||
|
.filter_by(exchange_id=exchange.id, email=email)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_participant:
|
||||||
|
# Participant already registered
|
||||||
|
flash(
|
||||||
|
"You're already registered for this exchange. "
|
||||||
|
"If you need access, you can request a new magic link below.",
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
return render_template(
|
||||||
|
"participant/register.html",
|
||||||
|
exchange=exchange,
|
||||||
|
form=form,
|
||||||
|
)
|
||||||
|
|
||||||
# Create participant record
|
# Create participant record
|
||||||
participant = Participant(
|
participant = Participant(
|
||||||
exchange_id=exchange.id,
|
exchange_id=exchange.id,
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
"""Tests for Story 4.3: Returning Participant Detection.
|
||||||
|
|
||||||
|
As a potential participant who has already registered, I want to be notified
|
||||||
|
if I try to register again so that I don't create duplicate entries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from src.models.exchange import Exchange
|
||||||
|
from src.models.participant import Participant
|
||||||
|
|
||||||
|
|
||||||
|
class TestStory43ReturningParticipantDetection:
|
||||||
|
"""Test suite for Story 4.3: Returning Participant Detection."""
|
||||||
|
|
||||||
|
def test_duplicate_email_shows_message(self, client, db):
|
||||||
|
"""Test that registering with existing email shows message."""
|
||||||
|
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||||
|
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||||
|
|
||||||
|
exchange = Exchange(
|
||||||
|
slug="winter2025",
|
||||||
|
name="Office Secret Santa 2025",
|
||||||
|
budget="$50",
|
||||||
|
max_participants=10,
|
||||||
|
registration_close_date=future_close_date,
|
||||||
|
exchange_date=future_exchange_date,
|
||||||
|
timezone="America/New_York",
|
||||||
|
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||||
|
)
|
||||||
|
db.session.add(exchange)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Create existing participant
|
||||||
|
existing_participant = Participant(
|
||||||
|
exchange_id=exchange.id,
|
||||||
|
name="John Doe",
|
||||||
|
email="john.doe@example.com",
|
||||||
|
gift_ideas="Books",
|
||||||
|
reminder_enabled=True,
|
||||||
|
)
|
||||||
|
db.session.add(existing_participant)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Try to register again with same email (case insensitive)
|
||||||
|
response = client.post(
|
||||||
|
"/exchange/winter2025/register",
|
||||||
|
data={
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "John.Doe@Example.COM", # Different case
|
||||||
|
"gift_ideas": "Gadgets",
|
||||||
|
},
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should show message that user is already registered
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"already registered" in response.data
|
||||||
|
|
||||||
|
# Should not create duplicate participant
|
||||||
|
participants = (
|
||||||
|
db.session.query(Participant).filter_by(email="john.doe@example.com").all()
|
||||||
|
)
|
||||||
|
assert len(participants) == 1
|
||||||
|
|
||||||
|
def test_duplicate_detection_offers_magic_link_request(self, client, db):
|
||||||
|
"""Test that duplicate detection page offers link to request magic link."""
|
||||||
|
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||||
|
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||||
|
|
||||||
|
exchange = Exchange(
|
||||||
|
slug="test123",
|
||||||
|
name="Test Exchange",
|
||||||
|
budget="$30",
|
||||||
|
max_participants=10,
|
||||||
|
registration_close_date=future_close_date,
|
||||||
|
exchange_date=future_exchange_date,
|
||||||
|
timezone="America/New_York",
|
||||||
|
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||||
|
)
|
||||||
|
db.session.add(exchange)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Create existing participant
|
||||||
|
participant = Participant(
|
||||||
|
exchange_id=exchange.id,
|
||||||
|
name="Jane Smith",
|
||||||
|
email="jane@example.com",
|
||||||
|
reminder_enabled=True,
|
||||||
|
)
|
||||||
|
db.session.add(participant)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Try to register again
|
||||||
|
response = client.post(
|
||||||
|
"/exchange/test123/register",
|
||||||
|
data={
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"email": "jane@example.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should show link to request access
|
||||||
|
assert (
|
||||||
|
b"request a new magic link" in response.data
|
||||||
|
or b"request access" in response.data
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_different_emails_can_register(self, client, db):
|
||||||
|
"""Test that different emails can register successfully."""
|
||||||
|
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||||
|
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||||
|
|
||||||
|
exchange = Exchange(
|
||||||
|
slug="test123",
|
||||||
|
name="Test Exchange",
|
||||||
|
budget="$30",
|
||||||
|
max_participants=10,
|
||||||
|
registration_close_date=future_close_date,
|
||||||
|
exchange_date=future_exchange_date,
|
||||||
|
timezone="America/New_York",
|
||||||
|
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||||
|
)
|
||||||
|
db.session.add(exchange)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Register first user
|
||||||
|
participant1 = Participant(
|
||||||
|
exchange_id=exchange.id,
|
||||||
|
name="Alice",
|
||||||
|
email="alice@example.com",
|
||||||
|
reminder_enabled=True,
|
||||||
|
)
|
||||||
|
db.session.add(participant1)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Register second user with different email (should succeed)
|
||||||
|
response = client.post(
|
||||||
|
"/exchange/test123/register",
|
||||||
|
data={
|
||||||
|
"name": "Bob",
|
||||||
|
"email": "bob@example.com",
|
||||||
|
},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should redirect to success page
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert "/exchange/test123/register/success" in response.location
|
||||||
|
|
||||||
|
# Should have two participants
|
||||||
|
participants = db.session.query(Participant).all()
|
||||||
|
assert len(participants) == 2
|
||||||
|
|
||||||
|
def test_same_email_different_exchanges_allowed(self, client, db):
|
||||||
|
"""Test that same email can register for different exchanges."""
|
||||||
|
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||||
|
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||||
|
|
||||||
|
# Create two exchanges
|
||||||
|
exchange1 = Exchange(
|
||||||
|
slug="exchange1",
|
||||||
|
name="Exchange 1",
|
||||||
|
budget="$30",
|
||||||
|
max_participants=10,
|
||||||
|
registration_close_date=future_close_date,
|
||||||
|
exchange_date=future_exchange_date,
|
||||||
|
timezone="America/New_York",
|
||||||
|
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||||
|
)
|
||||||
|
exchange2 = Exchange(
|
||||||
|
slug="exchange2",
|
||||||
|
name="Exchange 2",
|
||||||
|
budget="$40",
|
||||||
|
max_participants=10,
|
||||||
|
registration_close_date=future_close_date,
|
||||||
|
exchange_date=future_exchange_date,
|
||||||
|
timezone="America/New_York",
|
||||||
|
state=Exchange.STATE_REGISTRATION_OPEN,
|
||||||
|
)
|
||||||
|
db.session.add(exchange1)
|
||||||
|
db.session.add(exchange2)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Register for first exchange
|
||||||
|
participant1 = Participant(
|
||||||
|
exchange_id=exchange1.id,
|
||||||
|
name="John Doe",
|
||||||
|
email="john@example.com",
|
||||||
|
reminder_enabled=True,
|
||||||
|
)
|
||||||
|
db.session.add(participant1)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Register same email for second exchange (should succeed)
|
||||||
|
response = client.post(
|
||||||
|
"/exchange/exchange2/register",
|
||||||
|
data={
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should redirect to success page
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert "/exchange/exchange2/register/success" in response.location
|
||||||
|
|
||||||
|
# Should have two participants with same email but different exchanges
|
||||||
|
participants = (
|
||||||
|
db.session.query(Participant).filter_by(email="john@example.com").all()
|
||||||
|
)
|
||||||
|
assert len(participants) == 2
|
||||||
|
assert participants[0].exchange_id != participants[1].exchange_id
|
||||||
Reference in New Issue
Block a user