""" Модель вопроса """ 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