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:
2025-12-22 17:05:09 -07:00
parent abed4ac84a
commit 81e2cb8c86
6 changed files with 294 additions and 7 deletions

View File

@@ -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)

View File

@@ -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
View 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",
)

View File

@@ -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,
)

View 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 %}