feat: implement Story 4.1 - Access Registration Page
Allows potential participants to view exchange details and access the registration form via unique slug URLs. Implementation: - Added ParticipantRegistrationForm with name, email, gift_ideas, and reminder fields - Created GET /exchange/<slug>/register route - Built responsive registration template with exchange details - Exempted participant routes from admin setup requirement - Comprehensive test coverage for all scenarios Acceptance Criteria Met: - Valid slug displays registration form with exchange details - Invalid slug returns 404 - Form includes all required fields with CSRF protection - Registration deadline is displayed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
11
src/app.py
11
src/app.py
@@ -134,7 +134,16 @@ def register_setup_check(app: Flask) -> None:
|
||||
has been set up with an admin account.
|
||||
"""
|
||||
# Skip check for certain endpoints
|
||||
if request.endpoint in ["setup.setup", "static", "health"]:
|
||||
# Participant routes are public and don't require setup
|
||||
if request.endpoint in [
|
||||
"setup.setup",
|
||||
"static",
|
||||
"health",
|
||||
"participant.register",
|
||||
"participant.request_access",
|
||||
"participant.magic_login",
|
||||
"participant.dashboard",
|
||||
]:
|
||||
return
|
||||
|
||||
# Check if admin exists (always check in testing mode)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from src.forms.exchange import ExchangeForm
|
||||
from src.forms.login import LoginForm
|
||||
from src.forms.participant import ParticipantRegistrationForm
|
||||
from src.forms.setup import SetupForm
|
||||
|
||||
__all__ = ["ExchangeForm", "LoginForm", "SetupForm"]
|
||||
__all__ = ["ExchangeForm", "LoginForm", "ParticipantRegistrationForm", "SetupForm"]
|
||||
|
||||
37
src/forms/participant.py
Normal file
37
src/forms/participant.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Forms for participant registration and management."""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, EmailField, StringField, TextAreaField
|
||||
from wtforms.validators import DataRequired, Email, Length
|
||||
|
||||
|
||||
class ParticipantRegistrationForm(FlaskForm):
|
||||
"""Form for participant registration."""
|
||||
|
||||
name = StringField(
|
||||
"Name",
|
||||
validators=[
|
||||
DataRequired(message="Name is required"),
|
||||
Length(max=255, message="Name must be less than 255 characters"),
|
||||
],
|
||||
)
|
||||
|
||||
email = EmailField(
|
||||
"Email",
|
||||
validators=[
|
||||
DataRequired(message="Email is required"),
|
||||
Email(message="Please enter a valid email address"),
|
||||
Length(max=255, message="Email must be less than 255 characters"),
|
||||
],
|
||||
)
|
||||
|
||||
gift_ideas = TextAreaField(
|
||||
"Gift Ideas",
|
||||
description="Optional: Share ideas to help your Secret Santa",
|
||||
)
|
||||
|
||||
reminder_enabled = BooleanField(
|
||||
"Send me reminders",
|
||||
default=True,
|
||||
description="Receive email reminders about important dates",
|
||||
)
|
||||
@@ -1,13 +1,17 @@
|
||||
"""Participant routes for Sneaky Klaus application."""
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import Blueprint, abort, render_template
|
||||
|
||||
from src.app import db
|
||||
from src.forms.participant import ParticipantRegistrationForm
|
||||
from src.models.exchange import Exchange
|
||||
|
||||
participant_bp = Blueprint("participant", __name__, url_prefix="")
|
||||
|
||||
|
||||
@participant_bp.route("/exchange/<slug>/register")
|
||||
def register(slug):
|
||||
"""Participant registration page (stub for now).
|
||||
@participant_bp.route("/exchange/<slug>/register", methods=["GET", "POST"])
|
||||
def register(slug: str):
|
||||
"""Participant registration page.
|
||||
|
||||
Args:
|
||||
slug: Exchange registration slug.
|
||||
@@ -15,4 +19,16 @@ def register(slug):
|
||||
Returns:
|
||||
Rendered registration page template.
|
||||
"""
|
||||
return f"Registration page for exchange: {slug}"
|
||||
# Find the exchange by slug
|
||||
exchange = db.session.query(Exchange).filter_by(slug=slug).first()
|
||||
if not exchange:
|
||||
abort(404)
|
||||
|
||||
# Create the registration form
|
||||
form = ParticipantRegistrationForm()
|
||||
|
||||
return render_template(
|
||||
"participant/register.html",
|
||||
exchange=exchange,
|
||||
form=form,
|
||||
)
|
||||
|
||||
72
src/templates/participant/register.html
Normal file
72
src/templates/participant/register.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}Register for {{ exchange.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header>
|
||||
<h1>{{ exchange.name }}</h1>
|
||||
{% if exchange.description %}
|
||||
<p>{{ exchange.description }}</p>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>Exchange Details</h2>
|
||||
<dl>
|
||||
<dt>Budget</dt>
|
||||
<dd>{{ exchange.budget }}</dd>
|
||||
|
||||
<dt>Gift Exchange Date</dt>
|
||||
<dd>{{ exchange.exchange_date.strftime('%Y-%m-%d') }}</dd>
|
||||
|
||||
<dt>Registration Deadline</dt>
|
||||
<dd>{{ exchange.registration_close_date.strftime('%Y-%m-%d') }}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Register</h2>
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<label for="name">
|
||||
{{ form.name.label }}
|
||||
{{ form.name(placeholder="Your name") }}
|
||||
{% if form.name.errors %}
|
||||
<small>{{ form.name.errors[0] }}</small>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
<label for="email">
|
||||
{{ form.email.label }}
|
||||
{{ form.email(placeholder="your.email@example.com") }}
|
||||
{% if form.email.errors %}
|
||||
<small>{{ form.email.errors[0] }}</small>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
<label for="gift_ideas">
|
||||
{{ form.gift_ideas.label }}
|
||||
{{ form.gift_ideas(placeholder="Things you like, hobbies, interests...", rows="4") }}
|
||||
{% if form.gift_ideas.description %}
|
||||
<small>{{ form.gift_ideas.description }}</small>
|
||||
{% endif %}
|
||||
{% if form.gift_ideas.errors %}
|
||||
<small>{{ form.gift_ideas.errors[0] }}</small>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
<label for="reminder_enabled">
|
||||
{{ form.reminder_enabled() }}
|
||||
{{ form.reminder_enabled.label }}
|
||||
{% if form.reminder_enabled.description %}
|
||||
<small>{{ form.reminder_enabled.description }}</small>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
152
tests/integration/test_story_4_1_access_registration_page.py
Normal file
152
tests/integration/test_story_4_1_access_registration_page.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Tests for Story 4.1: Access Registration Page.
|
||||
|
||||
As a potential participant, I want to access the registration page
|
||||
via the unique link shared by the admin so that I can sign up for
|
||||
the Secret Santa exchange.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from src.models.exchange import Exchange
|
||||
|
||||
|
||||
class TestStory41AccessRegistrationPage:
|
||||
"""Test suite for Story 4.1: Access Registration Page."""
|
||||
|
||||
def test_get_registration_page_with_valid_slug(self, client, db):
|
||||
"""Test registration page with valid slug displays exchange details."""
|
||||
# Create an exchange
|
||||
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",
|
||||
description="Annual office gift exchange",
|
||||
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.commit()
|
||||
|
||||
# Access registration page
|
||||
response = client.get("/exchange/winter2025/register")
|
||||
|
||||
# Should return 200 OK
|
||||
assert response.status_code == 200
|
||||
|
||||
# Should display exchange details
|
||||
assert b"Office Secret Santa 2025" in response.data
|
||||
assert b"Annual office gift exchange" in response.data
|
||||
assert b"$50" in response.data
|
||||
# Format the date to match what will be displayed
|
||||
assert future_exchange_date.strftime("%Y-%m-%d").encode() in response.data
|
||||
|
||||
# Should display registration form
|
||||
assert b'name="name"' in response.data
|
||||
assert b'name="email"' in response.data
|
||||
assert b'name="gift_ideas"' in response.data
|
||||
assert b'name="reminder_enabled"' in response.data
|
||||
|
||||
def test_get_registration_page_with_invalid_slug(self, client):
|
||||
"""Test accessing registration page with invalid slug returns 404."""
|
||||
response = client.get("/exchange/invalid-slug/register")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_registration_form_has_csrf_protection(self, client, db):
|
||||
"""Test registration form includes POST method (CSRF via Flask-WTF)."""
|
||||
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.commit()
|
||||
|
||||
response = client.get("/exchange/test123/register")
|
||||
|
||||
# Should have a POST form (CSRF protection is provided by Flask-WTF)
|
||||
# Note: CSRF tokens are disabled in test mode, so we just verify the form exists
|
||||
assert b'<form method="POST">' in response.data
|
||||
assert b'<button type="submit">Register</button>' in response.data
|
||||
|
||||
def test_registration_page_displays_optional_description(self, client, db):
|
||||
"""Test that optional description is displayed if present."""
|
||||
future_close_date = datetime.utcnow() + timedelta(days=7)
|
||||
future_exchange_date = datetime.utcnow() + timedelta(days=14)
|
||||
|
||||
exchange = Exchange(
|
||||
slug="test123",
|
||||
name="Test Exchange",
|
||||
description="This is a test description",
|
||||
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.commit()
|
||||
|
||||
response = client.get("/exchange/test123/register")
|
||||
|
||||
assert b"This is a test description" in response.data
|
||||
|
||||
def test_registration_page_without_description(self, client, db):
|
||||
"""Test that page works when description is not provided."""
|
||||
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.commit()
|
||||
|
||||
response = client.get("/exchange/test123/register")
|
||||
|
||||
# Should still work without description
|
||||
assert response.status_code == 200
|
||||
assert b"Test Exchange" in response.data
|
||||
|
||||
def test_registration_page_displays_registration_deadline(self, client, db):
|
||||
"""Test that registration deadline is displayed."""
|
||||
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.commit()
|
||||
|
||||
response = client.get("/exchange/test123/register")
|
||||
|
||||
assert future_close_date.strftime("%Y-%m-%d").encode() in response.data
|
||||
Reference in New Issue
Block a user