460 lines
18 KiB
Python
460 lines
18 KiB
Python
"""
|
||
API маршруты для работы с пользователями, сообщениями и настройками
|
||
"""
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||
from sqlalchemy.orm import Session
|
||
from typing import List, Optional
|
||
import sys
|
||
import os
|
||
|
||
# Добавляем путь к корневой директории проекта
|
||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
from database.database import db
|
||
from database.models import User, Message, BotSettings
|
||
from api.schemas import (
|
||
UserCreate, UserUpdate, UserResponse,
|
||
MessageCreate, MessageResponse,
|
||
BotSettingCreate, BotSettingUpdate, BotSettingResponse,
|
||
MessageResponseModel, ErrorResponse
|
||
)
|
||
|
||
# Зависимость для получения сессии БД
|
||
def get_db():
|
||
"""Получение сессии базы данных"""
|
||
session = db.get_session_sync()
|
||
try:
|
||
yield session
|
||
finally:
|
||
session.close()
|
||
|
||
# Роутер для работы с пользователями
|
||
users_router = APIRouter(prefix="/users", tags=["Users"])
|
||
|
||
@users_router.get("/{telegram_id}", response_model=UserResponse)
|
||
async def get_user_by_telegram_id(telegram_id: int, db_session: Session = Depends(get_db)):
|
||
"""Получение пользователя по Telegram ID"""
|
||
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
|
||
if not user:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Пользователь не найден"
|
||
)
|
||
return user
|
||
|
||
@users_router.get("/", response_model=List[UserResponse])
|
||
async def get_all_users(
|
||
limit: int = Query(default=100, ge=1, le=1000),
|
||
offset: int = Query(default=0, ge=0),
|
||
is_active: Optional[bool] = Query(default=None),
|
||
db_session: Session = Depends(get_db)
|
||
):
|
||
"""Получение списка всех пользователей с фильтрацией"""
|
||
query = db_session.query(User)
|
||
|
||
if is_active is not None:
|
||
query = query.filter(User.is_active == is_active)
|
||
|
||
users = query.offset(offset).limit(limit).all()
|
||
return users
|
||
|
||
@users_router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||
async def create_user(user_data: UserCreate, db_session: Session = Depends(get_db)):
|
||
"""Создание нового пользователя"""
|
||
# Проверяем, существует ли уже пользователь
|
||
existing_user = db_session.query(User).filter(User.telegram_id == user_data.telegram_id).first()
|
||
if existing_user:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail="Пользователь уже существует"
|
||
)
|
||
|
||
# Создаем нового пользователя
|
||
new_user = User(**user_data.model_dump())
|
||
db_session.add(new_user)
|
||
db_session.commit()
|
||
db_session.refresh(new_user)
|
||
|
||
return new_user
|
||
|
||
@users_router.put("/{telegram_id}", response_model=UserResponse)
|
||
async def update_user(
|
||
telegram_id: int,
|
||
user_data: UserUpdate,
|
||
db_session: Session = Depends(get_db)
|
||
):
|
||
"""Обновление данных пользователя"""
|
||
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
|
||
if not user:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Пользователь не найден"
|
||
)
|
||
|
||
# Обновляем только переданные поля
|
||
update_data = user_data.model_dump(exclude_unset=True)
|
||
for field, value in update_data.items():
|
||
setattr(user, field, value)
|
||
|
||
db_session.commit()
|
||
db_session.refresh(user)
|
||
|
||
return user
|
||
|
||
@users_router.delete("/{telegram_id}", response_model=MessageResponseModel)
|
||
async def delete_user(telegram_id: int, db_session: Session = Depends(get_db)):
|
||
"""Деактивация пользователя (мягкое удаление)"""
|
||
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
|
||
if not user:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Пользователь не найден"
|
||
)
|
||
|
||
user.is_active = False
|
||
db_session.commit()
|
||
|
||
return MessageResponseModel(message="Пользователь деактивирован")
|
||
|
||
# Роутер для работы с сообщениями
|
||
messages_router = APIRouter(prefix="/messages", tags=["Messages"])
|
||
|
||
@messages_router.get("/", response_model=List[MessageResponse])
|
||
async def get_messages(
|
||
user_id: Optional[int] = Query(default=None),
|
||
message_type: Optional[str] = Query(default=None),
|
||
limit: int = Query(default=100, ge=1, le=1000),
|
||
offset: int = Query(default=0, ge=0),
|
||
db_session: Session = Depends(get_db)
|
||
):
|
||
"""Получение сообщений с фильтрацией"""
|
||
query = db_session.query(Message)
|
||
|
||
if user_id:
|
||
query = query.filter(Message.user_id == user_id)
|
||
|
||
if message_type:
|
||
query = query.filter(Message.message_type == message_type)
|
||
|
||
messages = query.order_by(Message.created_at.desc()).offset(offset).limit(limit).all()
|
||
return messages
|
||
|
||
@messages_router.get("/{message_id}", response_model=MessageResponse)
|
||
async def get_message(message_id: int, db_session: Session = Depends(get_db)):
|
||
"""Получение конкретного сообщения по ID"""
|
||
message = db_session.query(Message).filter(Message.id == message_id).first()
|
||
if not message:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Сообщение не найдено"
|
||
)
|
||
return message
|
||
|
||
@messages_router.post("/", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
|
||
async def create_message(message_data: MessageCreate, db_session: Session = Depends(get_db)):
|
||
"""Создание нового сообщения"""
|
||
# Проверяем, существует ли пользователь
|
||
user = db_session.query(User).filter(User.id == message_data.user_id).first()
|
||
if not user:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Пользователь не найден"
|
||
)
|
||
|
||
new_message = Message(**message_data.model_dump())
|
||
db_session.add(new_message)
|
||
db_session.commit()
|
||
db_session.refresh(new_message)
|
||
|
||
return new_message
|
||
|
||
# Роутер для работы с настройками бота
|
||
settings_router = APIRouter(prefix="/settings", tags=["Bot Settings"])
|
||
|
||
@settings_router.get("/", response_model=dict)
|
||
async def get_all_settings(db_session: Session = Depends(get_db)):
|
||
"""Получение всех настроек бота в виде словаря"""
|
||
settings = db_session.query(BotSettings).all()
|
||
return {setting.key: setting.value for setting in settings}
|
||
|
||
@settings_router.get("/list", response_model=List[BotSettingResponse])
|
||
async def get_all_settings_list(db_session: Session = Depends(get_db)):
|
||
"""Получение всех настроек бота в виде списка объектов"""
|
||
settings = db_session.query(BotSettings).all()
|
||
return settings
|
||
|
||
@settings_router.get("/{key}", response_model=BotSettingResponse)
|
||
async def get_setting(key: str, db_session: Session = Depends(get_db)):
|
||
"""Получение конкретной настройки по ключу"""
|
||
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
|
||
if not setting:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Настройка не найдена"
|
||
)
|
||
return setting
|
||
|
||
@settings_router.post("/", response_model=BotSettingResponse, status_code=status.HTTP_201_CREATED)
|
||
async def create_or_update_setting(
|
||
setting_data: BotSettingCreate,
|
||
db_session: Session = Depends(get_db)
|
||
):
|
||
"""Создание или обновление настройки"""
|
||
existing_setting = db_session.query(BotSettings).filter(BotSettings.key == setting_data.key).first()
|
||
|
||
if existing_setting:
|
||
# Обновляем существующую настройку
|
||
update_data = setting_data.model_dump(exclude={"key"})
|
||
for field, value in update_data.items():
|
||
if value is not None:
|
||
setattr(existing_setting, field, value)
|
||
db_session.commit()
|
||
db_session.refresh(existing_setting)
|
||
return existing_setting
|
||
else:
|
||
# Создаем новую настройку
|
||
new_setting = BotSettings(**setting_data.model_dump())
|
||
db_session.add(new_setting)
|
||
db_session.commit()
|
||
db_session.refresh(new_setting)
|
||
return new_setting
|
||
|
||
@settings_router.put("/{key}", response_model=BotSettingResponse)
|
||
async def update_setting(
|
||
key: str,
|
||
setting_data: BotSettingUpdate,
|
||
db_session: Session = Depends(get_db)
|
||
):
|
||
"""Обновление существующей настройки"""
|
||
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
|
||
if not setting:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Настройка не найдена"
|
||
)
|
||
|
||
update_data = setting_data.model_dump(exclude_unset=True)
|
||
for field, value in update_data.items():
|
||
setattr(setting, field, value)
|
||
|
||
db_session.commit()
|
||
db_session.refresh(setting)
|
||
|
||
return setting
|
||
|
||
@settings_router.delete("/{key}", response_model=MessageResponseModel)
|
||
async def delete_setting(key: str, db_session: Session = Depends(get_db)):
|
||
"""Удаление настройки"""
|
||
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
|
||
if not setting:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="Настройка не найдена"
|
||
)
|
||
|
||
db_session.delete(setting)
|
||
db_session.commit()
|
||
|
||
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}"}
|
||
) |