Automated Action 1754fec627 Create Bible Quiz App API with FastAPI and SQLite
- Set up project structure with FastAPI and SQLite
- Create models for users, questions, and quizzes
- Implement Alembic migrations with seed data
- Add user authentication with JWT
- Implement question management endpoints
- Implement quiz creation and management
- Add quiz-taking and scoring functionality
- Set up API documentation and health check endpoint
- Update README with comprehensive documentation
2025-06-03 15:46:44 +00:00

319 lines
9.0 KiB
Python

from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Path, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_active_user
from app.db.session import get_db
from app.models.user import User
from app.schemas.quiz import (
Quiz,
QuizAttempt,
QuizCreate,
QuizSubmitAnswer,
QuizUpdate,
QuizWithoutDetails,
)
from app.services import quiz as quiz_service
router = APIRouter()
@router.get("/", response_model=List[QuizWithoutDetails])
def read_quizzes(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve all quizzes accessible to the current user.
Includes all public quizzes and the user's own quizzes.
"""
# Get public quizzes
public_quizzes = quiz_service.get_public_quizzes(db, skip=skip, limit=limit)
# Get user's quizzes
user_quizzes = quiz_service.get_user_quizzes(db, user_id=current_user.id, skip=skip, limit=limit)
# Combine and deduplicate (using dict comprehension)
quizzes = {quiz.id: quiz for quiz in public_quizzes + user_quizzes}.values()
# Add question count
result = []
for quiz in quizzes:
setattr(quiz, "question_count", len(quiz.questions))
result.append(quiz)
return result
@router.post("/", response_model=Quiz)
def create_quiz(
*,
db: Session = Depends(get_db),
quiz_in: QuizCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new quiz.
"""
quiz = quiz_service.create(db, obj_in=quiz_in, user_id=current_user.id)
return quiz
@router.get("/{quiz_id}", response_model=Quiz)
def read_quiz(
*,
db: Session = Depends(get_db),
quiz_id: int = Path(..., gt=0),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get quiz by ID.
"""
quiz = quiz_service.get_by_id(db, quiz_id=quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
# Check if the user can access this quiz
if not quiz.is_public and quiz.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to access this quiz",
)
return quiz
@router.put("/{quiz_id}", response_model=Quiz)
def update_quiz(
*,
db: Session = Depends(get_db),
quiz_id: int = Path(..., gt=0),
quiz_in: QuizUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a quiz.
"""
quiz = quiz_service.get_by_id(db, quiz_id=quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
# Only the quiz owner can update it
if quiz.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to update this quiz",
)
quiz = quiz_service.update(db, db_obj=quiz, obj_in=quiz_in)
return quiz
@router.delete("/{quiz_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def delete_quiz(
*,
db: Session = Depends(get_db),
quiz_id: int = Path(..., gt=0),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a quiz.
"""
quiz = quiz_service.get_by_id(db, quiz_id=quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
# Only the quiz owner can delete it
if quiz.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to delete this quiz",
)
quiz_service.remove(db, quiz_id=quiz_id)
return None
# Quiz attempt endpoints
@router.get("/attempts/", response_model=List[QuizAttempt])
def read_user_attempts(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
quiz_id: Optional[int] = None,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve user's quiz attempts.
"""
attempts = quiz_service.get_user_attempts(
db,
user_id=current_user.id,
quiz_id=quiz_id,
skip=skip,
limit=limit,
)
return attempts
@router.post("/{quiz_id}/start", response_model=QuizAttempt)
def start_quiz_attempt(
*,
db: Session = Depends(get_db),
quiz_id: int = Path(..., gt=0),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Start a new quiz attempt.
"""
quiz = quiz_service.get_by_id(db, quiz_id=quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
# Check if the user can access this quiz
if not quiz.is_public and quiz.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to access this quiz",
)
attempt = quiz_service.create_attempt(
db,
obj_in={"quiz_id": quiz_id, "status": "started"},
user_id=current_user.id,
)
return attempt
@router.get("/attempts/{attempt_id}", response_model=QuizAttempt)
def read_quiz_attempt(
*,
db: Session = Depends(get_db),
attempt_id: int = Path(..., gt=0),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get quiz attempt by ID.
"""
attempt = quiz_service.get_attempt_by_id(db, attempt_id=attempt_id)
if not attempt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz attempt not found",
)
# Only the attempt owner can access it
if attempt.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to access this quiz attempt",
)
return attempt
@router.post("/attempts/{attempt_id}/submit/{question_id}", response_model=dict)
def submit_quiz_answer(
*,
db: Session = Depends(get_db),
attempt_id: int = Path(..., gt=0),
question_id: int = Path(..., gt=0),
answer_in: QuizSubmitAnswer,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Submit an answer for a quiz question.
"""
attempt = quiz_service.get_attempt_by_id(db, attempt_id=attempt_id)
if not attempt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz attempt not found",
)
# Only the attempt owner can submit answers
if attempt.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to submit answers for this attempt",
)
# Check if the attempt is still in progress
if attempt.status != "started":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cannot submit answers for an attempt with status '{attempt.status}'",
)
answer = quiz_service.submit_answer(
db,
attempt_id=attempt_id,
question_id=question_id,
selected_option_id=answer_in.selected_option_id,
)
if not answer:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid question or option ID",
)
return {"status": "success", "is_correct": answer.is_correct}
@router.post("/attempts/{attempt_id}/complete", response_model=dict)
def complete_quiz_attempt(
*,
db: Session = Depends(get_db),
attempt_id: int = Path(..., gt=0),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Complete a quiz attempt and calculate the final score.
"""
attempt = quiz_service.get_attempt_by_id(db, attempt_id=attempt_id)
if not attempt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz attempt not found",
)
# Only the attempt owner can complete it
if attempt.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to complete this attempt",
)
# Check if the attempt is still in progress
if attempt.status != "started":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cannot complete an attempt with status '{attempt.status}'",
)
attempt, score = quiz_service.complete_attempt(db, attempt_id=attempt_id)
return {
"status": "completed",
"score": score,
"passed": score >= attempt.quiz.pass_percentage,
"pass_percentage": attempt.quiz.pass_percentage,
}