
- 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
319 lines
9.0 KiB
Python
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,
|
|
} |