Files
AnonBot/models/question.py

119 lines
4.8 KiB
Python

"""
Модель вопроса
"""
import asyncio
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Optional
from config.constants import DEFAULT_QUESTION_PREVIEW_LENGTH, EMPTY_VALUES
class QuestionStatus(Enum):
"""Статусы вопроса"""
PENDING = "pending" # Ожидает ответа
ANSWERED = "answered" # Отвечен
REJECTED = "rejected" # Отклонен
DELETED = "deleted" # Удален
@dataclass
class Question:
"""Модель вопроса"""
id: Optional[int] = None
from_user_id: Optional[int] = None # ID отправителя (может быть None для анонимных)
to_user_id: int = None # ID получателя
message_text: str = "" # Текст вопроса
answer_text: Optional[str] = None # Текст ответа
is_anonymous: bool = True # Анонимный ли вопрос
message_id: Optional[int] = None # ID сообщения в Telegram
created_at: Optional[datetime] = None
answered_at: Optional[datetime] = None
is_read: bool = False # Прочитан ли вопрос
status: QuestionStatus = QuestionStatus.PENDING
user_question_number: Optional[int] = None # Локальный номер вопроса для пользователя
# Lazy loading атрибуты
_from_user: Optional['User'] = None
_to_user: Optional['User'] = None
_user_loader: Optional[callable] = None
@property
def is_answered(self) -> bool:
"""Проверка, отвечен ли вопрос"""
return self.status == QuestionStatus.ANSWERED
@property
def is_pending(self) -> bool:
"""Проверка, ожидает ли вопрос ответа"""
return self.status == QuestionStatus.PENDING
@classmethod
def _parse_datetime(cls, date_str) -> Optional[datetime]:
"""Безопасный парсинг datetime из строки"""
if not date_str or date_str in EMPTY_VALUES:
return None
try:
return datetime.fromisoformat(date_str)
except (ValueError, TypeError):
return None
@classmethod
async def _parse_datetime_async(cls, date_str) -> Optional[datetime]:
"""Асинхронный безопасный парсинг datetime из строки"""
if not date_str or date_str in EMPTY_VALUES:
return None
try:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, datetime.fromisoformat, date_str)
except (ValueError, TypeError):
return None
def mark_as_answered(self, answer_text: str):
"""Отметить вопрос как отвеченный"""
self.answer_text = answer_text
self.status = QuestionStatus.ANSWERED
self.answered_at = datetime.now()
def mark_as_rejected(self):
"""Отметить вопрос как отклоненный"""
self.status = QuestionStatus.REJECTED
self.answered_at = datetime.now()
def mark_as_deleted(self):
"""Отметить вопрос как удаленный"""
self.status = QuestionStatus.DELETED
self.answered_at = datetime.now()
self.user_question_number = None # Удаленные вопросы не имеют номера
def set_user_loader(self, loader_func: callable):
"""Установка функции для загрузки пользователей"""
self._user_loader = loader_func
async def get_from_user(self) -> Optional['User']:
"""Lazy loading автора вопроса"""
if self._from_user is None and self.from_user_id and self._user_loader:
self._from_user = await self._user_loader(self.from_user_id)
return self._from_user
async def get_to_user(self) -> Optional['User']:
"""Lazy loading получателя вопроса"""
if self._to_user is None and self.to_user_id and self._user_loader:
self._to_user = await self._user_loader(self.to_user_id)
return self._to_user
def get_question_preview(self, max_length: int = DEFAULT_QUESTION_PREVIEW_LENGTH) -> str:
"""Получить превью вопроса"""
if len(self.message_text) <= max_length:
return self.message_text
return self.message_text[:max_length] + "..."
def get_display_number(self) -> int:
"""Получить номер вопроса для отображения (приоритет user_question_number)"""
return self.user_question_number if self.user_question_number is not None else self.id