Automated Action 53d9909fb6 Create gamified kids learning API with FastAPI and SQLite
- Set up project structure with FastAPI and SQLite
- Implement user authentication with JWT
- Create models for learning content (subjects, lessons, quizzes)
- Add progress tracking and gamification features
- Implement comprehensive API documentation
- Add error handling and validation
- Set up proper logging and health check endpoint
2025-06-17 18:25:16 +00:00

516 lines
15 KiB
Python

from __future__ import annotations
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from sqlalchemy.orm import Session
from app import models, schemas
from app.api import deps
from app.db.session import get_db
from app.services.lesson import lesson as lesson_service
from app.services.question import question as question_service
from app.services.quiz import quiz as quiz_service
from app.services.subject import subject as subject_service
router = APIRouter()
# Subject endpoints
@router.get("/subjects", response_model=List[schemas.Subject])
def read_subjects(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
active_only: bool = Query(True, description="Filter only active subjects"),
) -> Any:
"""
Retrieve subjects.
"""
if active_only:
subjects = subject_service.get_active(db, skip=skip, limit=limit)
else:
subjects = subject_service.get_multi(db, skip=skip, limit=limit)
return subjects
@router.post("/subjects", response_model=schemas.Subject)
def create_subject(
*,
db: Session = Depends(get_db),
subject_in: schemas.SubjectCreate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create new subject. Only for superusers.
"""
subject = subject_service.get_by_name(db, name=subject_in.name)
if subject:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Subject with this name already exists",
)
subject = subject_service.create(db, obj_in=subject_in)
return subject
@router.get("/subjects/{subject_id}", response_model=schemas.Subject)
def read_subject(
*,
db: Session = Depends(get_db),
subject_id: int = Path(..., gt=0),
) -> Any:
"""
Get subject by ID.
"""
subject = subject_service.get(db, id=subject_id)
if not subject:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Subject not found",
)
return subject
@router.put("/subjects/{subject_id}", response_model=schemas.Subject)
def update_subject(
*,
db: Session = Depends(get_db),
subject_id: int = Path(..., gt=0),
subject_in: schemas.SubjectUpdate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update subject. Only for superusers.
"""
subject = subject_service.get(db, id=subject_id)
if not subject:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Subject not found",
)
# Check if name is being changed and already exists
if subject_in.name is not None and subject_in.name != subject.name:
existing_subject = subject_service.get_by_name(db, name=subject_in.name)
if existing_subject:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Subject with this name already exists",
)
subject = subject_service.update(db, db_obj=subject, obj_in=subject_in)
return subject
@router.delete(
"/subjects/{subject_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None
)
def delete_subject(
*,
db: Session = Depends(get_db),
subject_id: int = Path(..., gt=0),
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> None:
"""
Delete subject. Only for superusers.
"""
subject = subject_service.get(db, id=subject_id)
if not subject:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Subject not found",
)
subject_service.remove(db, id=subject_id)
# Lesson endpoints
@router.get("/subjects/{subject_id}/lessons", response_model=List[schemas.Lesson])
def read_lessons_by_subject(
*,
db: Session = Depends(get_db),
subject_id: int = Path(..., gt=0),
skip: int = 0,
limit: int = 100,
active_only: bool = Query(True, description="Filter only active lessons"),
) -> Any:
"""
Retrieve lessons for a subject.
"""
subject = subject_service.get(db, id=subject_id)
if not subject:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Subject not found",
)
if active_only:
lessons = lesson_service.get_active_by_subject(
db, subject_id=subject_id, skip=skip, limit=limit
)
else:
lessons = lesson_service.get_by_subject(db, subject_id=subject_id, skip=skip, limit=limit)
return lessons
@router.post("/lessons", response_model=schemas.Lesson)
def create_lesson(
*,
db: Session = Depends(get_db),
lesson_in: schemas.LessonCreate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create new lesson. Only for superusers.
"""
# Check if subject exists
subject = subject_service.get(db, id=lesson_in.subject_id)
if not subject:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Subject not found",
)
lesson = lesson_service.create(db, obj_in=lesson_in)
return lesson
@router.get("/lessons/{lesson_id}", response_model=schemas.Lesson)
def read_lesson(
*,
db: Session = Depends(get_db),
lesson_id: int = Path(..., gt=0),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get lesson by ID.
"""
lesson = lesson_service.get(db, id=lesson_id)
if not lesson:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Lesson not found",
)
return lesson
@router.put("/lessons/{lesson_id}", response_model=schemas.Lesson)
def update_lesson(
*,
db: Session = Depends(get_db),
lesson_id: int = Path(..., gt=0),
lesson_in: schemas.LessonUpdate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update lesson. Only for superusers.
"""
lesson = lesson_service.get(db, id=lesson_id)
if not lesson:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Lesson not found",
)
# Check if subject exists if it's being changed
if lesson_in.subject_id is not None and lesson_in.subject_id != lesson.subject_id:
subject = subject_service.get(db, id=lesson_in.subject_id)
if not subject:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Subject not found",
)
lesson = lesson_service.update(db, db_obj=lesson, obj_in=lesson_in)
return lesson
@router.delete("/lessons/{lesson_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def delete_lesson(
*,
db: Session = Depends(get_db),
lesson_id: int = Path(..., gt=0),
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> None:
"""
Delete lesson. Only for superusers.
"""
lesson = lesson_service.get(db, id=lesson_id)
if not lesson:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Lesson not found",
)
lesson_service.remove(db, id=lesson_id)
# Quiz endpoints
@router.get("/lessons/{lesson_id}/quizzes", response_model=List[schemas.Quiz])
def read_quizzes_by_lesson(
*,
db: Session = Depends(get_db),
lesson_id: int = Path(..., gt=0),
skip: int = 0,
limit: int = 100,
active_only: bool = Query(True, description="Filter only active quizzes"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Retrieve quizzes for a lesson.
"""
lesson = lesson_service.get(db, id=lesson_id)
if not lesson:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Lesson not found",
)
if active_only:
quizzes = quiz_service.get_active_by_lesson(db, lesson_id=lesson_id, skip=skip, limit=limit)
else:
quizzes = quiz_service.get_by_lesson(db, lesson_id=lesson_id, skip=skip, limit=limit)
return quizzes
@router.post("/quizzes", response_model=schemas.Quiz)
def create_quiz(
*,
db: Session = Depends(get_db),
quiz_in: schemas.QuizCreate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create new quiz. Only for superusers.
"""
# Check if lesson exists
lesson = lesson_service.get(db, id=quiz_in.lesson_id)
if not lesson:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Lesson not found",
)
quiz = quiz_service.create(db, obj_in=quiz_in)
return quiz
@router.get("/quizzes/{quiz_id}", response_model=schemas.Quiz)
def read_quiz(
*,
db: Session = Depends(get_db),
quiz_id: int = Path(..., gt=0),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get quiz by ID.
"""
quiz = quiz_service.get(db, id=quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
return quiz
@router.put("/quizzes/{quiz_id}", response_model=schemas.Quiz)
def update_quiz(
*,
db: Session = Depends(get_db),
quiz_id: int = Path(..., gt=0),
quiz_in: schemas.QuizUpdate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update quiz. Only for superusers.
"""
quiz = quiz_service.get(db, id=quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
# Check if lesson exists if it's being changed
if quiz_in.lesson_id is not None and quiz_in.lesson_id != quiz.lesson_id:
lesson = lesson_service.get(db, id=quiz_in.lesson_id)
if not lesson:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Lesson not found",
)
quiz = quiz_service.update(db, db_obj=quiz, obj_in=quiz_in)
return quiz
@router.delete("/quizzes/{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: models.User = Depends(deps.get_current_active_superuser),
) -> None:
"""
Delete quiz. Only for superusers.
"""
quiz = quiz_service.get(db, id=quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
quiz_service.remove(db, id=quiz_id)
# Question endpoints
@router.get("/quizzes/{quiz_id}/questions", response_model=List[schemas.Question])
def read_questions_by_quiz(
*,
db: Session = Depends(get_db),
quiz_id: int = Path(..., gt=0),
skip: int = 0,
limit: int = 100,
active_only: bool = Query(True, description="Filter only active questions"),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Retrieve questions for a quiz.
"""
quiz = quiz_service.get(db, id=quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
if active_only:
questions = question_service.get_active_by_quiz(db, quiz_id=quiz_id, skip=skip, limit=limit)
else:
questions = question_service.get_by_quiz(db, quiz_id=quiz_id, skip=skip, limit=limit)
return questions
@router.post("/questions", response_model=schemas.Question)
def create_question(
*,
db: Session = Depends(get_db),
question_in: schemas.QuestionCreate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create new question with answers. Only for superusers.
"""
# Check if quiz exists
quiz = quiz_service.get(db, id=question_in.quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
# Check if at least one answer is marked as correct
has_correct_answer = False
for answer in question_in.answers:
if answer.is_correct:
has_correct_answer = True
break
if not has_correct_answer:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="At least one answer must be marked as correct",
)
question = question_service.create_with_answers(db, obj_in=question_in)
return question
@router.get("/questions/{question_id}", response_model=schemas.Question)
def read_question(
*,
db: Session = Depends(get_db),
question_id: int = Path(..., gt=0),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get question by ID.
"""
question = question_service.get(db, id=question_id)
if not question:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Question not found",
)
return question
@router.put("/questions/{question_id}", response_model=schemas.Question)
def update_question(
*,
db: Session = Depends(get_db),
question_id: int = Path(..., gt=0),
question_in: schemas.QuestionUpdate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update question with answers. Only for superusers.
"""
question = question_service.get(db, id=question_id)
if not question:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Question not found",
)
# Check if quiz exists if it's being changed
if question_in.quiz_id is not None and question_in.quiz_id != question.quiz_id:
quiz = quiz_service.get(db, id=question_in.quiz_id)
if not quiz:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Quiz not found",
)
# Check if at least one answer is marked as correct if answers are being updated
if question_in.answers:
has_correct_answer = False
for answer in question_in.answers:
if answer.is_correct:
has_correct_answer = True
break
if not has_correct_answer:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="At least one answer must be marked as correct",
)
question = question_service.update_with_answers(db, db_obj=question, obj_in=question_in)
return question
@router.delete(
"/questions/{question_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None
)
def delete_question(
*,
db: Session = Depends(get_db),
question_id: int = Path(..., gt=0),
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> None:
"""
Delete question. Only for superusers.
"""
question = question_service.get(db, id=question_id)
if not question:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Question not found",
)
question_service.remove(db, id=question_id)