add algam
This commit is contained in:
43
api/Dockerfile
Normal file
43
api/Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# Dockerfile для API сервера
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Устанавливаем рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Устанавливаем системные зависимости
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Копируем файлы зависимостей
|
||||
COPY requirements.txt* ./
|
||||
COPY pyproject.toml* ./
|
||||
|
||||
# Устанавливаем Python зависимости
|
||||
RUN pip install --no-cache-dir --upgrade pip
|
||||
RUN if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi
|
||||
RUN if [ -f pyproject.toml ]; then pip install --no-cache-dir -e .; fi
|
||||
|
||||
# Устанавливаем основные зависимости для FastAPI
|
||||
RUN pip install --no-cache-dir \
|
||||
fastapi \
|
||||
uvicorn[standard] \
|
||||
sqlalchemy \
|
||||
python-multipart \
|
||||
python-dotenv
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
# Создаем директорию для данных
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Экспонируем порт
|
||||
EXPOSE 8000
|
||||
|
||||
# Проверка здоровья
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# Команда запуска
|
||||
CMD ["python", "run_api.py"]
|
||||
13
api/main.py
13
api/main.py
@@ -15,7 +15,11 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database.database import db, get_db_session, init_database, test_db_connection
|
||||
from database.models import User, Message, BotSettings
|
||||
from api.routers import users_router, messages_router, settings_router
|
||||
from api.routers import (
|
||||
users_router, messages_router, settings_router,
|
||||
test_categories_router, tests_router, questions_router,
|
||||
answer_options_router, images_router
|
||||
)
|
||||
from api.schemas import HealthCheckResponse, RootResponse
|
||||
from api.config import config
|
||||
|
||||
@@ -70,6 +74,13 @@ app.include_router(users_router, prefix=config.API_V1_PREFIX)
|
||||
app.include_router(messages_router, prefix=config.API_V1_PREFIX)
|
||||
app.include_router(settings_router, prefix=config.API_V1_PREFIX)
|
||||
|
||||
# Подключение роутеров для системы тестирования
|
||||
app.include_router(test_categories_router, prefix=config.API_V1_PREFIX)
|
||||
app.include_router(tests_router, prefix=config.API_V1_PREFIX)
|
||||
app.include_router(questions_router, prefix=config.API_V1_PREFIX)
|
||||
app.include_router(answer_options_router, prefix=config.API_V1_PREFIX)
|
||||
app.include_router(images_router, prefix=config.API_V1_PREFIX)
|
||||
|
||||
@app.get("/", response_model=RootResponse)
|
||||
async def root():
|
||||
"""Корневой эндпоинт"""
|
||||
|
||||
203
api/routers.py
203
api/routers.py
@@ -256,4 +256,205 @@ async def delete_setting(key: str, db_session: Session = Depends(get_db)):
|
||||
db_session.delete(setting)
|
||||
db_session.commit()
|
||||
|
||||
return MessageResponseModel(message=f"Настройка '{key}' удалена")
|
||||
return MessageResponseModel(message=f"Настройка '{key}' удалена")
|
||||
|
||||
|
||||
# Добавляем импорты для работы с тестами
|
||||
from database.models import Test, TestCategory, Question, AnswerOption, Image
|
||||
from database.services import ImageService, TestService, QuestionService
|
||||
from database.repositories import TestRepository, QuestionRepository, ImageRepository
|
||||
from api.schemas import (
|
||||
TestCategoryCreate, TestCategoryResponse, TestCreate, TestResponse,
|
||||
QuestionCreate, QuestionResponse, AnswerOptionCreate, AnswerOptionResponse,
|
||||
ImageUploadResponse
|
||||
)
|
||||
from fastapi import UploadFile, File
|
||||
|
||||
# Роутер для работы с категориями тестов
|
||||
test_categories_router = APIRouter(prefix="/test-categories", tags=["Test Categories"])
|
||||
|
||||
@test_categories_router.post("/", response_model=TestCategoryResponse)
|
||||
async def create_test_category(
|
||||
category_data: TestCategoryCreate,
|
||||
db_session: Session = Depends(get_db)
|
||||
):
|
||||
"""Создание новой категории тестов"""
|
||||
category = TestCategory(
|
||||
name=category_data.name,
|
||||
description=category_data.description
|
||||
)
|
||||
db_session.add(category)
|
||||
db_session.commit()
|
||||
db_session.refresh(category)
|
||||
return category
|
||||
|
||||
@test_categories_router.get("/", response_model=List[TestCategoryResponse])
|
||||
async def get_test_categories(db_session: Session = Depends(get_db)):
|
||||
"""Получение всех категорий тестов"""
|
||||
categories = db_session.query(TestCategory).all()
|
||||
return categories
|
||||
|
||||
# Роутер для работы с тестами
|
||||
tests_router = APIRouter(prefix="/tests", tags=["Tests"])
|
||||
|
||||
@tests_router.post("/", response_model=TestResponse)
|
||||
async def create_test(
|
||||
test_data: TestCreate,
|
||||
db_session: Session = Depends(get_db)
|
||||
):
|
||||
"""Создание нового теста"""
|
||||
test_repo = TestRepository(db_session)
|
||||
test = test_repo.create_test(
|
||||
title=test_data.title,
|
||||
description=test_data.description,
|
||||
category_id=test_data.category_id,
|
||||
is_active=test_data.is_active,
|
||||
time_limit_minutes=test_data.time_limit_minutes
|
||||
)
|
||||
return test
|
||||
|
||||
@tests_router.get("/", response_model=List[TestResponse])
|
||||
async def get_tests(
|
||||
is_active: Optional[bool] = Query(default=None),
|
||||
category_id: Optional[int] = Query(default=None),
|
||||
db_session: Session = Depends(get_db)
|
||||
):
|
||||
"""Получение списка тестов с фильтрацией"""
|
||||
test_repo = TestRepository(db_session)
|
||||
tests = test_repo.get_tests(is_active=is_active, category_id=category_id)
|
||||
return tests
|
||||
|
||||
@tests_router.get("/{test_id}", response_model=TestResponse)
|
||||
async def get_test_by_id(test_id: int, db_session: Session = Depends(get_db)):
|
||||
"""Получение теста по ID"""
|
||||
test_repo = TestRepository(db_session)
|
||||
test = test_repo.get_test_by_id(test_id)
|
||||
if not test:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Тест не найден"
|
||||
)
|
||||
return test
|
||||
|
||||
# Роутер для работы с вопросами
|
||||
questions_router = APIRouter(prefix="/questions", tags=["Questions"])
|
||||
|
||||
@questions_router.post("/", response_model=QuestionResponse)
|
||||
async def create_question(
|
||||
question_data: QuestionCreate,
|
||||
db_session: Session = Depends(get_db)
|
||||
):
|
||||
"""Создание нового вопроса"""
|
||||
question_repo = QuestionRepository(db_session)
|
||||
question = question_repo.create_question(
|
||||
test_id=question_data.test_id,
|
||||
question_text=question_data.question_text,
|
||||
question_type=question_data.question_type,
|
||||
order_number=question_data.order_number,
|
||||
points=question_data.points,
|
||||
image_id=question_data.image_id
|
||||
)
|
||||
return question
|
||||
|
||||
@questions_router.get("/test/{test_id}", response_model=List[QuestionResponse])
|
||||
async def get_questions_by_test(test_id: int, db_session: Session = Depends(get_db)):
|
||||
"""Получение всех вопросов для теста"""
|
||||
question_repo = QuestionRepository(db_session)
|
||||
questions = question_repo.get_questions_by_test_id(test_id)
|
||||
return questions
|
||||
|
||||
@questions_router.get("/{question_id}", response_model=QuestionResponse)
|
||||
async def get_question_by_id(question_id: int, db_session: Session = Depends(get_db)):
|
||||
"""Получение вопроса по ID"""
|
||||
question_repo = QuestionRepository(db_session)
|
||||
question = question_repo.get_question_by_id(question_id)
|
||||
if not question:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Вопрос не найден"
|
||||
)
|
||||
return question
|
||||
|
||||
# Роутер для работы с вариантами ответов
|
||||
answer_options_router = APIRouter(prefix="/answer-options", tags=["Answer Options"])
|
||||
|
||||
@answer_options_router.post("/", response_model=AnswerOptionResponse)
|
||||
async def create_answer_option(
|
||||
option_data: AnswerOptionCreate,
|
||||
db_session: Session = Depends(get_db)
|
||||
):
|
||||
"""Создание нового варианта ответа"""
|
||||
option = AnswerOption(
|
||||
question_id=option_data.question_id,
|
||||
option_text=option_data.option_text,
|
||||
is_correct=option_data.is_correct,
|
||||
order_number=option_data.order_number
|
||||
)
|
||||
db_session.add(option)
|
||||
db_session.commit()
|
||||
db_session.refresh(option)
|
||||
return option
|
||||
|
||||
@answer_options_router.get("/question/{question_id}", response_model=List[AnswerOptionResponse])
|
||||
async def get_answer_options_by_question(question_id: int, db_session: Session = Depends(get_db)):
|
||||
"""Получение всех вариантов ответов для вопроса"""
|
||||
options = db_session.query(AnswerOption).filter(
|
||||
AnswerOption.question_id == question_id
|
||||
).order_by(AnswerOption.order_number).all()
|
||||
return options
|
||||
|
||||
# Роутер для работы с изображениями
|
||||
images_router = APIRouter(prefix="/images", tags=["Images"])
|
||||
|
||||
@images_router.post("/upload", response_model=ImageUploadResponse)
|
||||
async def upload_image(
|
||||
file: UploadFile = File(...),
|
||||
alt_text: Optional[str] = None,
|
||||
db_session: Session = Depends(get_db)
|
||||
):
|
||||
"""Загрузка изображения в базу данных"""
|
||||
# Проверяем тип файла
|
||||
if not file.content_type.startswith('image/'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Загружаемый файл должен быть изображением"
|
||||
)
|
||||
|
||||
# Читаем содержимое файла
|
||||
image_data = await file.read()
|
||||
|
||||
# Сохраняем в БД через сервис
|
||||
image = ImageService.save_image_to_db(
|
||||
db=db_session,
|
||||
filename=file.filename,
|
||||
content_type=file.content_type,
|
||||
image_data=image_data,
|
||||
alt_text=alt_text
|
||||
)
|
||||
|
||||
return ImageUploadResponse(
|
||||
id=image.id,
|
||||
filename=image.filename,
|
||||
content_type=image.content_type,
|
||||
file_size=image.file_size
|
||||
)
|
||||
|
||||
@images_router.get("/{image_id}")
|
||||
async def get_image(image_id: int, db_session: Session = Depends(get_db)):
|
||||
"""Получение изображения по ID"""
|
||||
from fastapi.responses import Response
|
||||
|
||||
image_repo = ImageRepository(db_session)
|
||||
image = image_repo.get_image_by_id(image_id)
|
||||
|
||||
if not image:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Изображение не найдено"
|
||||
)
|
||||
|
||||
return Response(
|
||||
content=image.image_data,
|
||||
media_type=image.content_type,
|
||||
headers={"Content-Disposition": f"inline; filename={image.filename}"}
|
||||
)
|
||||
@@ -120,4 +120,89 @@ class PaginatedResponse(BaseModel):
|
||||
limit: int
|
||||
offset: int
|
||||
has_next: bool
|
||||
has_prev: bool
|
||||
has_prev: bool
|
||||
|
||||
# Схемы для работы с тестами
|
||||
|
||||
class TestCategoryCreate(BaseModel):
|
||||
"""Схема для создания категории тестов"""
|
||||
name: str = Field(..., max_length=100, description="Название категории")
|
||||
description: Optional[str] = Field(None, description="Описание категории")
|
||||
|
||||
class TestCategoryResponse(BaseModel):
|
||||
"""Схема ответа с категорией тестов"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
class TestCreate(BaseModel):
|
||||
"""Схема для создания теста"""
|
||||
title: str = Field(..., max_length=200, description="Название теста")
|
||||
description: Optional[str] = Field(None, description="Описание теста")
|
||||
category_id: Optional[int] = Field(None, description="ID категории")
|
||||
is_active: bool = Field(default=True, description="Активен ли тест")
|
||||
time_limit_minutes: Optional[int] = Field(None, ge=1, description="Лимит времени в минутах")
|
||||
|
||||
class TestResponse(BaseModel):
|
||||
"""Схема ответа с тестом"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
title: str
|
||||
description: Optional[str]
|
||||
category_id: Optional[int]
|
||||
is_active: bool
|
||||
time_limit_minutes: Optional[int]
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
|
||||
class QuestionCreate(BaseModel):
|
||||
"""Схема для создания вопроса"""
|
||||
test_id: int = Field(..., description="ID теста")
|
||||
question_text: str = Field(..., description="Текст вопроса")
|
||||
question_type: str = Field(default="single_choice", description="Тип вопроса")
|
||||
order_number: int = Field(..., ge=1, description="Порядковый номер вопроса")
|
||||
points: int = Field(default=1, ge=0, description="Количество баллов за правильный ответ")
|
||||
image_id: Optional[int] = Field(None, description="ID изображения к вопросу")
|
||||
|
||||
class QuestionResponse(BaseModel):
|
||||
"""Схема ответа с вопросом"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
test_id: int
|
||||
question_text: str
|
||||
question_type: str
|
||||
order_number: int
|
||||
points: int
|
||||
image_id: Optional[int]
|
||||
created_at: datetime
|
||||
|
||||
class AnswerOptionCreate(BaseModel):
|
||||
"""Схема для создания варианта ответа"""
|
||||
question_id: int = Field(..., description="ID вопроса")
|
||||
option_text: str = Field(..., description="Текст варианта ответа")
|
||||
is_correct: bool = Field(..., description="Правильный ли этот вариант")
|
||||
order_number: int = Field(..., ge=1, description="Порядковый номер варианта")
|
||||
|
||||
class AnswerOptionResponse(BaseModel):
|
||||
"""Схема ответа с вариантом ответа"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
question_id: int
|
||||
option_text: str
|
||||
is_correct: bool
|
||||
order_number: int
|
||||
created_at: datetime
|
||||
|
||||
class ImageUploadResponse(BaseModel):
|
||||
"""Схема ответа при загрузке изображения"""
|
||||
id: int
|
||||
filename: str
|
||||
content_type: str
|
||||
file_size: int
|
||||
message: str = "Изображение успешно загружено"
|
||||
Reference in New Issue
Block a user