Implement user-specific question numbering and update database schema. Added triggers for automatic question numbering and adjustments upon deletion. Enhanced CRUD operations to manage user_question_number effectively.

This commit is contained in:
2025-09-06 18:35:12 +03:00
parent 50be010026
commit 596a2fa813
111 changed files with 16847 additions and 65 deletions

118
models/question.py Normal file
View File

@@ -0,0 +1,118 @@
"""
Модель вопроса
"""
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