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:
@@ -1,6 +1,7 @@
|
||||
"""Forms for Sneaky Klaus application."""
|
||||
|
||||
from src.forms.exchange import ExchangeForm
|
||||
from src.forms.login import LoginForm
|
||||
from src.forms.setup import SetupForm
|
||||
|
||||
__all__ = ["LoginForm", "SetupForm"]
|
||||
__all__ = ["ExchangeForm", "LoginForm", "SetupForm"]
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Forms for exchange management."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytz # type: ignore[import-untyped]
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
DateTimeLocalField,
|
||||
IntegerField,
|
||||
SelectField,
|
||||
StringField,
|
||||
TextAreaField,
|
||||
)
|
||||
from wtforms.validators import DataRequired, Length, NumberRange, ValidationError
|
||||
|
||||
|
||||
class ExchangeForm(FlaskForm):
|
||||
"""Form for creating and editing exchanges.
|
||||
|
||||
Fields:
|
||||
name: Exchange name/title.
|
||||
description: Optional description.
|
||||
budget: Gift budget (freeform text).
|
||||
max_participants: Maximum participant limit.
|
||||
registration_close_date: When registration ends.
|
||||
exchange_date: When gifts are exchanged.
|
||||
timezone: IANA timezone name.
|
||||
"""
|
||||
|
||||
name = StringField(
|
||||
"Exchange Name",
|
||||
validators=[DataRequired(), Length(min=1, max=255)],
|
||||
)
|
||||
|
||||
description = TextAreaField(
|
||||
"Description",
|
||||
validators=[Length(max=2000)],
|
||||
)
|
||||
|
||||
budget = StringField(
|
||||
"Gift Budget",
|
||||
validators=[DataRequired(), Length(min=1, max=100)],
|
||||
)
|
||||
|
||||
max_participants = IntegerField(
|
||||
"Maximum Participants",
|
||||
validators=[
|
||||
DataRequired(),
|
||||
NumberRange(min=3, message="Must have at least 3 participants"),
|
||||
],
|
||||
)
|
||||
|
||||
registration_close_date = DateTimeLocalField(
|
||||
"Registration Close Date",
|
||||
validators=[DataRequired()],
|
||||
format="%Y-%m-%dT%H:%M",
|
||||
)
|
||||
|
||||
exchange_date = DateTimeLocalField(
|
||||
"Exchange Date",
|
||||
validators=[DataRequired()],
|
||||
format="%Y-%m-%dT%H:%M",
|
||||
)
|
||||
|
||||
timezone = SelectField(
|
||||
"Timezone",
|
||||
validators=[DataRequired()],
|
||||
choices=[], # Will be populated in __init__
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize form and populate timezone choices.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments for FlaskForm.
|
||||
**kwargs: Keyword arguments for FlaskForm.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Populate timezone choices with common timezones
|
||||
common_timezones = [
|
||||
"America/New_York",
|
||||
"America/Chicago",
|
||||
"America/Denver",
|
||||
"America/Los_Angeles",
|
||||
"America/Anchorage",
|
||||
"Pacific/Honolulu",
|
||||
"Europe/London",
|
||||
"Europe/Paris",
|
||||
"Europe/Berlin",
|
||||
"Asia/Tokyo",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Dubai",
|
||||
"Australia/Sydney",
|
||||
"Pacific/Auckland",
|
||||
]
|
||||
|
||||
# Add all pytz timezones to ensure validation works
|
||||
self.timezone.choices = [(tz, tz) for tz in common_timezones]
|
||||
|
||||
def validate_timezone(self, field):
|
||||
"""Validate that timezone is a valid IANA timezone.
|
||||
|
||||
Args:
|
||||
field: Timezone field to validate.
|
||||
|
||||
Raises:
|
||||
ValidationError: If timezone is not valid.
|
||||
"""
|
||||
try:
|
||||
pytz.timezone(field.data)
|
||||
except pytz.UnknownTimeZoneError as e:
|
||||
raise ValidationError(
|
||||
"Invalid timezone. Please select a valid timezone."
|
||||
) from e
|
||||
|
||||
def validate_exchange_date(self, field):
|
||||
"""Validate that exchange_date is after registration_close_date.
|
||||
|
||||
Args:
|
||||
field: Exchange date field to validate.
|
||||
|
||||
Raises:
|
||||
ValidationError: If exchange_date is not after registration_close_date.
|
||||
"""
|
||||
if (
|
||||
self.registration_close_date.data
|
||||
and field.data
|
||||
and field.data <= self.registration_close_date.data
|
||||
):
|
||||
raise ValidationError(
|
||||
"Exchange date must be after registration close date."
|
||||
)
|
||||
|
||||
def validate_registration_close_date(self, field):
|
||||
"""Validate that registration_close_date is in the future.
|
||||
|
||||
Args:
|
||||
field: Registration close date field to validate.
|
||||
|
||||
Raises:
|
||||
ValidationError: If registration_close_date is in the past.
|
||||
"""
|
||||
if field.data and field.data <= datetime.utcnow():
|
||||
raise ValidationError("Registration close date must be in the future.")
|
||||
Reference in New Issue
Block a user