feat: implement exchange creation

Add Exchange model, API endpoint, and form validation
for creating new gift exchanges.

- Create ExchangeForm with timezone validation
- Add admin routes for creating and viewing exchanges
- Generate unique 12-char slug for each exchange
- Validate registration/exchange dates
- Display exchanges in dashboard
- All tests passing with 92% coverage

Story: 2.1
This commit is contained in:
2025-12-22 12:41:28 -07:00
parent 7580c39a84
commit 8554f27d86
12 changed files with 837 additions and 7 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
"""Route blueprints for Sneaky Klaus application."""
from src.routes.admin import admin_bp
from src.routes.participant import participant_bp
from src.routes.setup import setup_bp
__all__ = ["setup_bp"]
__all__ = ["admin_bp", "participant_bp", "setup_bp"]
+82 -3
View File
@@ -6,8 +6,8 @@ from flask import Blueprint, flash, redirect, render_template, session, url_for
from src.app import bcrypt, db
from src.decorators import admin_required
from src.forms import LoginForm
from src.models import Admin
from src.forms import ExchangeForm, LoginForm
from src.models import Admin, Exchange
from src.utils import check_rate_limit, increment_rate_limit, reset_rate_limit
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
@@ -100,4 +100,83 @@ def dashboard():
Returns:
Rendered admin dashboard template.
"""
return render_template("admin/dashboard.html")
# Get all exchanges ordered by exchange_date
exchanges = db.session.query(Exchange).order_by(Exchange.exchange_date.asc()).all()
# Count exchanges by state
active_count = sum(
1
for e in exchanges
if e.state
in [
Exchange.STATE_REGISTRATION_OPEN,
Exchange.STATE_REGISTRATION_CLOSED,
Exchange.STATE_MATCHED,
]
)
completed_count = sum(1 for e in exchanges if e.state == Exchange.STATE_COMPLETED)
draft_count = sum(1 for e in exchanges if e.state == Exchange.STATE_DRAFT)
return render_template(
"admin/dashboard.html",
exchanges=exchanges,
active_count=active_count,
completed_count=completed_count,
draft_count=draft_count,
)
@admin_bp.route("/exchange/new", methods=["GET", "POST"])
@admin_required
def create_exchange():
"""Create a new exchange.
GET: Display exchange creation form.
POST: Process form and create exchange.
Returns:
On GET: Rendered form template.
On POST success: Redirect to exchange detail page.
On POST error: Re-render form with errors.
"""
form = ExchangeForm()
if form.validate_on_submit():
# Generate unique slug
slug = Exchange.generate_slug()
# Create new exchange
exchange = Exchange(
slug=slug,
name=form.name.data,
description=form.description.data or None,
budget=form.budget.data,
max_participants=form.max_participants.data,
registration_close_date=form.registration_close_date.data,
exchange_date=form.exchange_date.data,
timezone=form.timezone.data,
state=Exchange.STATE_DRAFT,
)
db.session.add(exchange)
db.session.commit()
flash("Exchange created successfully!", "success")
return redirect(url_for("admin.view_exchange", exchange_id=exchange.id))
return render_template("admin/exchange_form.html", form=form, is_edit=False)
@admin_bp.route("/exchange/<int:exchange_id>")
@admin_required
def view_exchange(exchange_id):
"""View exchange details.
Args:
exchange_id: ID of the exchange to view.
Returns:
Rendered exchange detail template.
"""
exchange = db.session.query(Exchange).get_or_404(exchange_id)
return render_template("admin/exchange_detail.html", exchange=exchange)
+18
View File
@@ -0,0 +1,18 @@
"""Participant routes for Sneaky Klaus application."""
from flask import Blueprint
participant_bp = Blueprint("participant", __name__, url_prefix="")
@participant_bp.route("/exchange/<slug>/register")
def register(slug):
"""Participant registration page (stub for now).
Args:
slug: Exchange registration slug.
Returns:
Rendered registration page template.
"""
return f"Registration page for exchange: {slug}"