
- 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
516 lines
15 KiB
Python
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)
|