119 lines
4.8 KiB
Python
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
|
|
|