Refactor event API and extend authentication utilities
All checks were successful
Build and Push Docker Images / changes (push) Successful in 6s
Build and Push Docker Images / build-backend (push) Successful in 55s
Build and Push Docker Images / build-frontend (push) Has been skipped

Refactored the event API routes to improve error handling, add logging, and provide enhanced response structures with pagination. Updated tests to use new fixtures and include additional authentication utilities to facilitate testing with FastAPI's dependency injection. Also resolved issues with timezone awareness in event schemas.
This commit is contained in:
2025-03-09 16:04:51 +01:00
parent fe2bcbd6e7
commit c2cdc3c110
7 changed files with 466 additions and 96 deletions

View File

@@ -1,24 +1,34 @@
from datetime import datetime
from typing import List, Optional
from datetime import timezone
from typing import Optional, Any, Dict
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from api.dependencies.auth import get_current_user
from app.api.dependencies.auth import get_current_user, get_optional_current_user
from app.core.database import get_db
from app.crud.event import event
from app.models.event_manager import EventManager
from app.models.user import User
from app.schemas.common import PaginatedResponse
from app.schemas.events import (
EventCreate,
EventUpdate,
EventResponse
EventResponse,
)
import logging
logger = logging.getLogger(__name__)
events_router = APIRouter()
@events_router.post("/", response_model=EventResponse, operation_id="create_event")
@events_router.post(
"/",
response_model=EventResponse,
status_code=status.HTTP_201_CREATED,
operation_id="create_event"
)
def create_event(
*,
db: Session = Depends(get_db),
@@ -26,50 +36,129 @@ def create_event(
current_user: User = Depends(get_current_user)
) -> EventResponse:
"""Create a new event."""
# Check if slug is already taken
if event.get_by_slug(db, slug=event_in.slug):
try:
# Check if slug is already taken
if event.get_by_slug(db, slug=event_in.slug):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="An event with this slug already exists"
)
created_event = event.create_with_owner(db=db, obj_in=event_in, owner_id=current_user.id)
logger.info(f"Event created by {current_user.email}: {created_event.slug}")
return created_event
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(
status_code=400,
detail="An event with this slug already exists"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Database error occurred"
)
return event.create_with_owner(
db=db,
obj_in=event_in,
owner_id=current_user.id
)
@events_router.get("/me", response_model=List[EventResponse], operation_id="get_user_events")
@events_router.get(
"/me",
response_model=PaginatedResponse[EventResponse],
operation_id="get_user_events"
)
def get_user_events(
*,
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
include_inactive: bool = False,
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
include_inactive: bool = Query(False),
current_user: User = Depends(get_current_user)
) -> List[EventResponse]:
"""Get all events created by the current user."""
return event.get_user_events(
db=db,
user_id=current_user.id,
skip=skip,
limit=limit,
include_inactive=include_inactive
)
) -> Dict[str, Any]:
"""Get all events created by the current user with pagination."""
try:
total = event.count_user_events(
db=db,
user_id=current_user.id,
include_inactive=include_inactive
)
items = event.get_user_events(
db=db,
user_id=current_user.id,
skip=skip,
limit=limit,
include_inactive=include_inactive
)
return {
"total": total,
"items": items,
"page": skip // limit + 1 if limit > 0 else 1,
"size": limit
}
except SQLAlchemyError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving events"
)
@events_router.get("/upcoming", response_model=List[EventResponse], operation_id="get_upcoming_events")
@events_router.get(
"/upcoming",
response_model=PaginatedResponse[EventResponse],
operation_id="get_upcoming_events"
)
def get_upcoming_events(
*,
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100
) -> List[EventResponse]:
"""Get upcoming public events."""
return event.get_upcoming_events(db=db, skip=skip, limit=limit)
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500)
) -> Dict[str, Any]:
"""Get upcoming public events with pagination."""
try:
items = event.get_upcoming_events(db=db, skip=skip, limit=limit)
# Count total upcoming events for pagination
total = event.count_upcoming_events(db=db)
return {
"total": total,
"items": items,
"page": skip // limit + 1 if limit > 0 else 1,
"size": limit
}
except SQLAlchemyError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving upcoming events"
)
@events_router.get("/{event_id}", response_model=EventResponse, operation_id="get_event")
@events_router.get(
"/public",
response_model=PaginatedResponse[EventResponse],
operation_id="get_public_events"
)
def get_public_events(
*,
db: Session = Depends(get_db),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500)
) -> Dict[str, Any]:
"""Get all public events with pagination."""
try:
items = event.get_public_events(db=db, skip=skip, limit=limit)
total = event.count_public_events(db=db)
return {
"total": total,
"items": items,
"page": skip // limit + 1 if limit > 0 else 1,
"size": limit
}
except SQLAlchemyError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving public events"
)
@events_router.get(
"/{event_id}",
response_model=EventResponse,
operation_id="get_event"
)
def get_event(
*,
db: Session = Depends(get_db),
@@ -77,27 +166,78 @@ def get_event(
current_user: User = Depends(get_current_user)
) -> EventResponse:
"""Get event by ID."""
event_obj = event.get(db=db, id=event_id)
if not event_obj:
raise HTTPException(status_code=404, detail="Event not found")
return event_obj
try:
event_obj = event.get(db=db, id=event_id)
if not event_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
# Check if user is creator or manager
if event_obj.created_by != current_user.id:
# Check if user is a manager
is_manager = db.query(EventManager).filter(
EventManager.event_id == event_id,
EventManager.user_id == current_user.id
).first()
if not is_manager and not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return event_obj
except SQLAlchemyError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving event"
)
@events_router.get("/by-slug/{slug}", response_model=EventResponse, operation_id="get_public_event")
@events_router.get(
"/by-slug/{slug}",
response_model=EventResponse,
operation_id="get_public_event"
)
def get_public_event(
*,
db: Session = Depends(get_db),
slug: str,
access_code: Optional[str] = Query(None)
access_code: Optional[str] = Query(None),
current_user: Optional[User] = Depends(get_optional_current_user)
) -> EventResponse:
"""Get public event by slug."""
event_obj = event.get_public_event(db=db, slug=slug, access_code=access_code)
if not event_obj:
raise HTTPException(status_code=404, detail="Event not found")
return event_obj
try:
event_obj = event.get_public_event(db=db, slug=slug, access_code=access_code)
if not event_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
# If event is not public and user is not authenticated, check access code
if not event_obj.is_public and not current_user:
if not access_code or access_code != event_obj.access_code:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid access code"
)
return event_obj
except SQLAlchemyError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving event"
)
@events_router.put("/{event_id}", response_model=EventResponse, operation_id="update_event")
@events_router.put(
"/{event_id}",
response_model=EventResponse,
operation_id="update_event"
)
def update_event(
*,
db: Session = Depends(get_db),
@@ -106,36 +246,95 @@ def update_event(
current_user: User = Depends(get_current_user)
) -> EventResponse:
"""Update event."""
event_obj = event.get(db=db, id=event_id)
if not event_obj:
raise HTTPException(status_code=404, detail="Event not found")
if event_obj.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# If slug is being updated, check if new slug is available
if event_in.slug and event_in.slug != event_obj.slug:
if event.get_by_slug(db, slug=event_in.slug):
try:
event_obj = event.get(db=db, id=event_id)
if not event_obj:
raise HTTPException(
status_code=400,
detail="An event with this slug already exists"
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
return event.update(db=db, db_obj=event_obj, obj_in=event_in)
# Check permissions (creator or manager with edit rights)
has_permission = False
if event_obj.created_by == current_user.id or current_user.is_superuser:
has_permission = True
else:
manager = db.query(EventManager).filter(
EventManager.event_id == event_id,
EventManager.user_id == current_user.id,
EventManager.can_edit == True
).first()
has_permission = manager is not None
if not has_permission:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions to edit this event"
)
# If slug is being updated, check if new slug is available and different
if event_in.slug and event_in.slug != event_obj.slug:
existing = event.get_by_slug(db, slug=event_in.slug)
if existing and existing.id != event_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="An event with this slug already exists"
)
return event.update(db=db, db_obj=event_obj, obj_in=event_in)
except SQLAlchemyError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error updating event"
)
@events_router.delete("/{event_id}", operation_id="delete_event")
@events_router.delete(
"/{event_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="delete_event"
)
def delete_event(
*,
db: Session = Depends(get_db),
event_id: UUID,
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
hard_delete: bool = Query(False, description="Perform hard delete instead of soft delete")
):
"""Delete event."""
event_obj = event.get(db=db, id=event_id)
if not event_obj:
raise HTTPException(status_code=404, detail="Event not found")
if event_obj.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
"""Delete event (soft delete by default)."""
try:
event_obj = event.get(db=db, id=event_id)
if not event_obj:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
event.remove(db=db, id=event_id)
return {"message": "Event deleted successfully"}
# Only creator or superuser can delete
if event_obj.created_by != current_user.id and not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions to delete this event"
)
if hard_delete:
# Hard delete - only for superusers
if not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can perform hard delete"
)
event.remove(db=db, id=event_id)
else:
# Soft delete - set is_active to False
event.update(db=db, db_obj=event_obj, obj_in={"is_active": False})
return None # 204 No Content
except SQLAlchemyError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error deleting event"
)