Basic event management
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from . import auth, users, tiers, memberships, payments, email, email_templates
|
||||
from . import auth, users, tiers, memberships, payments, email, email_templates, events
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -10,3 +10,4 @@ api_router.include_router(memberships.router, prefix="/memberships", tags=["memb
|
||||
api_router.include_router(payments.router, prefix="/payments", tags=["payments"])
|
||||
api_router.include_router(email.router, prefix="/email", tags=["email"])
|
||||
api_router.include_router(email_templates.router, prefix="/email-templates", tags=["email-templates"])
|
||||
api_router.include_router(events.router, prefix="/events", tags=["events"])
|
||||
|
||||
207
backend/app/api/v1/events.py
Normal file
207
backend/app/api/v1/events.py
Normal file
@@ -0,0 +1,207 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
from ...core.database import get_db
|
||||
from ...models.models import Event, EventRSVP, User, EventStatus
|
||||
from ...schemas import (
|
||||
EventCreate, EventUpdate, EventResponse, EventRSVPResponse, EventRSVPUpdate, MessageResponse
|
||||
)
|
||||
from ...api.dependencies import get_current_active_user, get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[EventResponse])
|
||||
async def get_events(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all events (admin) or published events (members)"""
|
||||
if current_user.role in ['admin', 'super_admin']:
|
||||
events = db.query(Event).order_by(Event.event_date).all()
|
||||
else:
|
||||
events = db.query(Event).filter(
|
||||
Event.status == EventStatus.PUBLISHED
|
||||
).order_by(Event.event_date).all()
|
||||
return events
|
||||
|
||||
|
||||
@router.get("/upcoming", response_model=List[EventResponse])
|
||||
async def get_upcoming_events(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get upcoming events"""
|
||||
now = datetime.now()
|
||||
events = db.query(Event).filter(
|
||||
Event.event_date >= now.date(),
|
||||
Event.status == EventStatus.PUBLISHED
|
||||
).order_by(Event.event_date).all()
|
||||
return events
|
||||
|
||||
|
||||
@router.post("/", response_model=EventResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_event(
|
||||
event_data: EventCreate,
|
||||
current_user: User = Depends(get_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new event (admin only)"""
|
||||
# Validate event date is in the future
|
||||
if event_data.event_date < datetime.now():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Event date must be in the future"
|
||||
)
|
||||
|
||||
event = Event(
|
||||
title=event_data.title,
|
||||
description=event_data.description,
|
||||
event_date=event_data.event_date,
|
||||
event_time=event_data.event_time,
|
||||
location=event_data.location,
|
||||
max_attendees=event_data.max_attendees,
|
||||
status=EventStatus.DRAFT,
|
||||
created_by=current_user.id
|
||||
)
|
||||
|
||||
db.add(event)
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
@router.put("/{event_id}", response_model=EventResponse)
|
||||
async def update_event(
|
||||
event_id: int,
|
||||
event_data: EventUpdate,
|
||||
current_user: User = Depends(get_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update an event (admin only)"""
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Event not found"
|
||||
)
|
||||
|
||||
# Update fields
|
||||
for field, value in event_data.dict(exclude_unset=True).items():
|
||||
setattr(event, field, value)
|
||||
|
||||
event.updated_at = datetime.now()
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
@router.delete("/{event_id}", response_model=MessageResponse)
|
||||
async def delete_event(
|
||||
event_id: int,
|
||||
current_user: User = Depends(get_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete an event (admin only)"""
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Event not found"
|
||||
)
|
||||
|
||||
db.delete(event)
|
||||
db.commit()
|
||||
return {"message": "Event deleted successfully"}
|
||||
|
||||
|
||||
@router.get("/{event_id}/rsvps", response_model=List[EventRSVPResponse])
|
||||
async def get_event_rsvps(
|
||||
event_id: int,
|
||||
current_user: User = Depends(get_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get RSVPs for an event (admin only)"""
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Event not found"
|
||||
)
|
||||
|
||||
rsvps = db.query(EventRSVP).filter(EventRSVP.event_id == event_id).all()
|
||||
return rsvps
|
||||
|
||||
|
||||
@router.post("/{event_id}/rsvp", response_model=EventRSVPResponse)
|
||||
async def create_or_update_rsvp(
|
||||
event_id: int,
|
||||
rsvp_data: EventRSVPUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create or update RSVP for an event"""
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Event not found"
|
||||
)
|
||||
|
||||
if event.status != EventStatus.PUBLISHED:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Event is not available for RSVP"
|
||||
)
|
||||
|
||||
# Check if RSVP already exists
|
||||
existing_rsvp = db.query(EventRSVP).filter(
|
||||
EventRSVP.event_id == event_id,
|
||||
EventRSVP.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if existing_rsvp:
|
||||
# Update existing RSVP
|
||||
existing_rsvp.status = rsvp_data.status
|
||||
if rsvp_data.notes is not None:
|
||||
existing_rsvp.notes = rsvp_data.notes
|
||||
existing_rsvp.updated_at = datetime.now()
|
||||
db.commit()
|
||||
db.refresh(existing_rsvp)
|
||||
return existing_rsvp
|
||||
else:
|
||||
# Check attendee limit
|
||||
if event.max_attendees:
|
||||
current_rsvp_count = db.query(EventRSVP).filter(
|
||||
EventRSVP.event_id == event_id,
|
||||
EventRSVP.status == 'attending'
|
||||
).count()
|
||||
if current_rsvp_count >= event.max_attendees:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Event is at maximum capacity"
|
||||
)
|
||||
|
||||
# Create new RSVP
|
||||
rsvp = EventRSVP(
|
||||
event_id=event_id,
|
||||
user_id=current_user.id,
|
||||
status=rsvp_data.status,
|
||||
notes=rsvp_data.notes
|
||||
)
|
||||
db.add(rsvp)
|
||||
db.commit()
|
||||
db.refresh(rsvp)
|
||||
return rsvp
|
||||
|
||||
|
||||
@router.get("/my-rsvps", response_model=List[EventRSVPResponse])
|
||||
async def get_my_rsvps(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get current user's RSVPs"""
|
||||
rsvps = db.query(EventRSVP).filter(EventRSVP.user_id == current_user.id).all()
|
||||
return rsvps
|
||||
@@ -41,7 +41,7 @@ class Settings(BaseSettings):
|
||||
FRONTEND_URL: str = "http://localhost:3500"
|
||||
|
||||
# CORS
|
||||
BACKEND_CORS_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8080"]
|
||||
BACKEND_CORS_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8080", "https://members.sasalliance.org"]
|
||||
|
||||
# File Storage
|
||||
UPLOAD_DIR: str = "/app/uploads"
|
||||
|
||||
@@ -135,6 +135,7 @@ class Event(Base):
|
||||
title = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
event_date = Column(DateTime, nullable=False)
|
||||
event_time = Column(String(10), nullable=True) # HH:MM format
|
||||
location = Column(String(255), nullable=True)
|
||||
max_attendees = Column(Integer, nullable=True)
|
||||
status = Column(SQLEnum(EventStatus, values_callable=lambda x: [e.value for e in x]), default=EventStatus.DRAFT, nullable=False)
|
||||
|
||||
@@ -30,6 +30,13 @@ from .schemas import (
|
||||
EmailTemplateCreate,
|
||||
EmailTemplateUpdate,
|
||||
EmailTemplateResponse,
|
||||
EventBase,
|
||||
EventCreate,
|
||||
EventUpdate,
|
||||
EventResponse,
|
||||
EventRSVPBase,
|
||||
EventRSVPUpdate,
|
||||
EventRSVPResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -64,4 +71,11 @@ __all__ = [
|
||||
"EmailTemplateCreate",
|
||||
"EmailTemplateUpdate",
|
||||
"EmailTemplateResponse",
|
||||
"EventBase",
|
||||
"EventCreate",
|
||||
"EventUpdate",
|
||||
"EventResponse",
|
||||
"EventRSVPBase",
|
||||
"EventRSVPUpdate",
|
||||
"EventRSVPResponse",
|
||||
]
|
||||
|
||||
@@ -229,3 +229,58 @@ class EmailTemplateResponse(EmailTemplateBase):
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
# Event Schemas
|
||||
class EventBase(BaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
event_date: datetime
|
||||
event_time: Optional[str] = Field(None, pattern=r'^([01]?[0-9]|2[0-3]):[0-5][0-9]$')
|
||||
location: Optional[str] = None
|
||||
max_attendees: Optional[int] = Field(None, gt=0)
|
||||
|
||||
|
||||
class EventCreate(EventBase):
|
||||
pass
|
||||
|
||||
|
||||
class EventUpdate(BaseModel):
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
event_date: Optional[datetime] = None
|
||||
event_time: Optional[str] = Field(None, pattern=r'^([01]?[0-9]|2[0-3]):[0-5][0-9]$')
|
||||
location: Optional[str] = None
|
||||
max_attendees: Optional[int] = Field(None, gt=0)
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class EventResponse(EventBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
status: str
|
||||
created_by: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
# Event RSVP Schemas
|
||||
class EventRSVPBase(BaseModel):
|
||||
status: str = Field(..., pattern="^(pending|attending|not_attending|maybe)$")
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class EventRSVPUpdate(EventRSVPBase):
|
||||
pass
|
||||
|
||||
|
||||
class EventRSVPResponse(EventRSVPBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
event_id: int
|
||||
user_id: int
|
||||
attended: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
Reference in New Issue
Block a user