bd - api
This commit is contained in:
358
database/services.py
Normal file
358
database/services.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""
|
||||
Сервисы для работы с системой тестирования
|
||||
Основная бизнес-логика для получения вопросов, сохранения ответов и работы с изображениями
|
||||
"""
|
||||
|
||||
import json
|
||||
import io
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
|
||||
from database.models import (
|
||||
User, Test, TestCategory, Question, AnswerOption,
|
||||
TestResult, UserAnswer, Image
|
||||
)
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageService:
|
||||
"""Сервис для работы с изображениями в базе данных"""
|
||||
|
||||
@staticmethod
|
||||
def save_image_to_db(
|
||||
db: Session,
|
||||
filename: str,
|
||||
content_type: str,
|
||||
image_data: bytes,
|
||||
alt_text: str = None
|
||||
) -> Image:
|
||||
"""
|
||||
Сохранение изображения в базу данных
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
filename: Имя файла
|
||||
content_type: MIME тип (image/jpeg, image/png и т.д.)
|
||||
image_data: Бинарные данные изображения
|
||||
alt_text: Альтернативный текст
|
||||
|
||||
Returns:
|
||||
Image: Созданная запись изображения
|
||||
"""
|
||||
image = Image(
|
||||
filename=filename,
|
||||
content_type=content_type,
|
||||
file_size=len(image_data),
|
||||
image_data=image_data,
|
||||
alt_text=alt_text
|
||||
)
|
||||
|
||||
db.add(image)
|
||||
db.commit()
|
||||
db.refresh(image)
|
||||
|
||||
return image
|
||||
|
||||
@staticmethod
|
||||
def get_image_data(db: Session, image_id: int) -> Optional[Tuple[bytes, str, str]]:
|
||||
"""
|
||||
Получение данных изображения из БД
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
image_id: ID изображения
|
||||
|
||||
Returns:
|
||||
Tuple[bytes, str, str]: (image_data, content_type, filename) или None
|
||||
"""
|
||||
image = db.query(Image).filter(Image.id == image_id).first()
|
||||
|
||||
if image:
|
||||
return image.image_data, image.content_type, image.filename
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_image_by_filename(db: Session, filename: str) -> Optional[Image]:
|
||||
"""Получение изображения по имени файла"""
|
||||
return db.query(Image).filter(Image.filename == filename).first()
|
||||
|
||||
|
||||
class TestService:
|
||||
"""Сервис для работы с тестами"""
|
||||
|
||||
@staticmethod
|
||||
def get_available_tests(db: Session) -> List[Test]:
|
||||
"""Получение всех активных тестов"""
|
||||
return db.query(Test).filter(Test.is_active == True).all()
|
||||
|
||||
@staticmethod
|
||||
def get_test_by_title(db: Session, title: str) -> Optional[Test]:
|
||||
"""Получение теста по названию (например, '📘 Тест Климова')"""
|
||||
return db.query(Test).filter(
|
||||
and_(Test.title == title, Test.is_active == True)
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def get_test_questions(db: Session, test_id: int) -> List[Question]:
|
||||
"""
|
||||
Получение всех вопросов теста в правильном порядке
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
test_id: ID теста
|
||||
|
||||
Returns:
|
||||
List[Question]: Список вопросов с загруженными связями
|
||||
"""
|
||||
return db.query(Question).filter(
|
||||
and_(Question.test_id == test_id, Question.is_active == True)
|
||||
).order_by(Question.order_index).all()
|
||||
|
||||
@staticmethod
|
||||
def start_test(db: Session, user_id: int, test_id: int) -> TestResult:
|
||||
"""
|
||||
Начало прохождения теста пользователем
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
test_id: ID теста
|
||||
|
||||
Returns:
|
||||
TestResult: Созданная запись результата теста
|
||||
"""
|
||||
# Проверяем, есть ли незавершенный тест
|
||||
existing_result = db.query(TestResult).filter(
|
||||
and_(
|
||||
TestResult.user_id == user_id,
|
||||
TestResult.test_id == test_id,
|
||||
TestResult.is_completed == False
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing_result:
|
||||
return existing_result
|
||||
|
||||
# Создаем новый результат теста
|
||||
test_result = TestResult(
|
||||
user_id=user_id,
|
||||
test_id=test_id,
|
||||
is_completed=False
|
||||
)
|
||||
|
||||
db.add(test_result)
|
||||
db.commit()
|
||||
db.refresh(test_result)
|
||||
|
||||
return test_result
|
||||
|
||||
@staticmethod
|
||||
def save_answer(
|
||||
db: Session,
|
||||
test_result_id: int,
|
||||
question_id: int,
|
||||
selected_option_id: int
|
||||
) -> UserAnswer:
|
||||
"""
|
||||
Сохранение ответа пользователя на вопрос
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
test_result_id: ID результата теста
|
||||
question_id: ID вопроса
|
||||
selected_option_id: ID выбранного варианта ответа
|
||||
|
||||
Returns:
|
||||
UserAnswer: Созданная запись ответа
|
||||
"""
|
||||
# Проверяем, есть ли уже ответ на этот вопрос
|
||||
existing_answer = db.query(UserAnswer).filter(
|
||||
and_(
|
||||
UserAnswer.test_result_id == test_result_id,
|
||||
UserAnswer.question_id == question_id
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing_answer:
|
||||
# Обновляем существующий ответ
|
||||
existing_answer.selected_option_id = selected_option_id
|
||||
db.commit()
|
||||
return existing_answer
|
||||
|
||||
# Создаем новый ответ
|
||||
user_answer = UserAnswer(
|
||||
test_result_id=test_result_id,
|
||||
question_id=question_id,
|
||||
selected_option_id=selected_option_id
|
||||
)
|
||||
|
||||
db.add(user_answer)
|
||||
db.commit()
|
||||
db.refresh(user_answer)
|
||||
|
||||
return user_answer
|
||||
|
||||
@staticmethod
|
||||
def complete_test(db: Session, test_result_id: int) -> TestResult:
|
||||
"""
|
||||
Завершение теста и подсчет результатов
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
test_result_id: ID результата теста
|
||||
|
||||
Returns:
|
||||
TestResult: Обновленная запись результата с подсчитанными данными
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
test_result = db.query(TestResult).filter(
|
||||
TestResult.id == test_result_id
|
||||
).first()
|
||||
|
||||
if not test_result:
|
||||
raise ValueError(f"TestResult с ID {test_result_id} не найден")
|
||||
|
||||
# Получаем все ответы пользователя для этого теста
|
||||
answers = db.query(UserAnswer).filter(
|
||||
UserAnswer.test_result_id == test_result_id
|
||||
).all()
|
||||
|
||||
# Подсчитываем результаты по категориям
|
||||
category_scores = {}
|
||||
|
||||
for answer in answers:
|
||||
option = db.query(AnswerOption).filter(
|
||||
AnswerOption.id == answer.selected_option_id
|
||||
).first()
|
||||
|
||||
if option and option.category:
|
||||
category = option.category
|
||||
category_scores[category] = category_scores.get(category, 0) + 1
|
||||
|
||||
# Сохраняем результаты в JSON формате
|
||||
test_result.result_data = json.dumps(category_scores, ensure_ascii=False)
|
||||
test_result.completed_at = datetime.now()
|
||||
test_result.is_completed = True
|
||||
|
||||
db.commit()
|
||||
db.refresh(test_result)
|
||||
|
||||
return test_result
|
||||
|
||||
@staticmethod
|
||||
def get_test_results(db: Session, test_result_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Получение и форматирование результатов теста
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
test_result_id: ID результата теста
|
||||
|
||||
Returns:
|
||||
Dict: Словарь с результатами и процентами
|
||||
"""
|
||||
test_result = db.query(TestResult).filter(
|
||||
TestResult.id == test_result_id
|
||||
).first()
|
||||
|
||||
if not test_result or not test_result.result_data:
|
||||
return {}
|
||||
|
||||
# Парсим результаты
|
||||
category_scores = json.loads(test_result.result_data)
|
||||
|
||||
# Получаем максимальные возможные баллы для каждой категории
|
||||
questions = TestService.get_test_questions(db, test_result.test_id)
|
||||
max_scores = {}
|
||||
|
||||
for question in questions:
|
||||
for option in question.answer_options:
|
||||
if option.category:
|
||||
max_scores[option.category] = max_scores.get(option.category, 0) + 1
|
||||
|
||||
# Вычисляем проценты
|
||||
results = {}
|
||||
for category, score in category_scores.items():
|
||||
max_score = max_scores.get(category, 1)
|
||||
percentage = (score / max_score) * 100
|
||||
results[category] = {
|
||||
'score': score,
|
||||
'max_score': max_score,
|
||||
'percentage': round(percentage, 1)
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def get_user_test_history(db: Session, user_id: int) -> List[TestResult]:
|
||||
"""Получение истории тестов пользователя"""
|
||||
return db.query(TestResult).filter(
|
||||
and_(TestResult.user_id == user_id, TestResult.is_completed == True)
|
||||
).order_by(TestResult.completed_at.desc()).all()
|
||||
|
||||
|
||||
class QuestionService:
|
||||
"""Сервис для работы с вопросами"""
|
||||
|
||||
@staticmethod
|
||||
def get_question_with_options(db: Session, question_id: int) -> Optional[Question]:
|
||||
"""
|
||||
Получение вопроса со всеми вариантами ответов и изображением
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
question_id: ID вопроса
|
||||
|
||||
Returns:
|
||||
Question: Вопрос с загруженными связями или None
|
||||
"""
|
||||
return db.query(Question).filter(Question.id == question_id).first()
|
||||
|
||||
@staticmethod
|
||||
def format_question_for_bot(db: Session, question: Question) -> Dict[str, Any]:
|
||||
"""
|
||||
Форматирование вопроса для отправки в бот
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
question: Объект вопроса
|
||||
|
||||
Returns:
|
||||
Dict: Сформатированные данные вопроса
|
||||
"""
|
||||
# Получаем варианты ответов
|
||||
options = db.query(AnswerOption).filter(
|
||||
AnswerOption.question_id == question.id
|
||||
).order_by(AnswerOption.order_index).all()
|
||||
|
||||
# Получаем данные изображения, если есть
|
||||
image_data = None
|
||||
if question.image_id:
|
||||
image_info = ImageService.get_image_data(db, question.image_id)
|
||||
if image_info:
|
||||
image_data = {
|
||||
'data': image_info[0],
|
||||
'content_type': image_info[1],
|
||||
'filename': image_info[2]
|
||||
}
|
||||
|
||||
return {
|
||||
'id': question.id,
|
||||
'text': question.question_text,
|
||||
'order_index': question.order_index,
|
||||
'image': image_data,
|
||||
'options': [
|
||||
{
|
||||
'id': option.id,
|
||||
'text': option.option_text,
|
||||
'category': option.category,
|
||||
'order_index': option.order_index
|
||||
}
|
||||
for option in options
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user