diff --git a/app/bot.py b/app/bot.py
index da9de335..f21b7534 100644
--- a/app/bot.py
+++ b/app/bot.py
@@ -17,7 +17,7 @@ from app.utils.cache import cache
from app.handlers import (
start, menu, subscription, balance, promocode,
- referral, support, server_status, common, tickets, polls
+ referral, support, server_status, common, tickets
)
from app.handlers import simple_subscription
from app.handlers.admin import (
@@ -48,7 +48,6 @@ from app.handlers.admin import (
privacy_policy as admin_privacy_policy,
public_offer as admin_public_offer,
faq as admin_faq,
- polls as admin_polls,
)
from app.handlers.stars_payments import register_stars_handlers
@@ -135,7 +134,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
support.register_handlers(dp)
server_status.register_handlers(dp)
tickets.register_handlers(dp)
- polls.register_handlers(dp)
admin_main.register_handlers(dp)
admin_users.register_handlers(dp)
admin_subscriptions.register_handlers(dp)
@@ -163,7 +161,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
admin_privacy_policy.register_handlers(dp)
admin_public_offer.register_handlers(dp)
admin_faq.register_handlers(dp)
- admin_polls.register_handlers(dp)
common.register_handlers(dp)
register_stars_handlers(dp)
simple_subscription.register_simple_subscription_handlers(dp)
diff --git a/app/database/models.py b/app/database/models.py
index d1283fc1..d296aba9 100644
--- a/app/database/models.py
+++ b/app/database/models.py
@@ -1061,7 +1061,7 @@ class PromoOfferLog(Base):
class BroadcastHistory(Base):
__tablename__ = "broadcast_history"
-
+
id = Column(Integer, primary_key=True, index=True)
target_type = Column(String(100), nullable=False)
message_text = Column(Text, nullable=False)
@@ -1079,130 +1079,6 @@ class BroadcastHistory(Base):
completed_at = Column(DateTime(timezone=True), nullable=True)
admin = relationship("User", back_populates="broadcasts")
-
-class Poll(Base):
- __tablename__ = "polls"
-
- id = Column(Integer, primary_key=True, index=True)
- title = Column(String(255), nullable=False)
- description = Column(Text, nullable=False)
- reward_enabled = Column(Boolean, default=False, nullable=False)
- reward_amount_kopeks = Column(Integer, default=0, nullable=False)
- created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
- created_at = Column(DateTime, default=func.now())
- updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
-
- questions = relationship(
- "PollQuestion",
- back_populates="poll",
- cascade="all, delete-orphan",
- order_by="PollQuestion.order",
- )
- runs = relationship(
- "PollRun",
- back_populates="poll",
- cascade="all, delete-orphan",
- order_by="PollRun.created_at.desc()",
- )
- responses = relationship(
- "PollResponse",
- back_populates="poll",
- cascade="all, delete-orphan",
- )
- creator = relationship("User", backref="created_polls")
-
-
-class PollQuestion(Base):
- __tablename__ = "poll_questions"
-
- id = Column(Integer, primary_key=True, index=True)
- poll_id = Column(Integer, ForeignKey("polls.id", ondelete="CASCADE"), nullable=False)
- order = Column(Integer, nullable=False, default=0)
- text = Column(Text, nullable=False)
-
- poll = relationship("Poll", back_populates="questions")
- options = relationship(
- "PollOption",
- back_populates="question",
- cascade="all, delete-orphan",
- order_by="PollOption.order",
- )
-
-
-class PollOption(Base):
- __tablename__ = "poll_options"
-
- id = Column(Integer, primary_key=True, index=True)
- question_id = Column(Integer, ForeignKey("poll_questions.id", ondelete="CASCADE"), nullable=False)
- order = Column(Integer, nullable=False, default=0)
- text = Column(String(255), nullable=False)
-
- question = relationship("PollQuestion", back_populates="options")
-
-
-class PollRun(Base):
- __tablename__ = "poll_runs"
-
- id = Column(Integer, primary_key=True, index=True)
- poll_id = Column(Integer, ForeignKey("polls.id", ondelete="CASCADE"), nullable=False)
- target_type = Column(String(100), nullable=False)
- status = Column(String(50), default="scheduled", nullable=False)
- total_count = Column(Integer, default=0)
- sent_count = Column(Integer, default=0)
- failed_count = Column(Integer, default=0)
- completed_count = Column(Integer, default=0)
- created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
- created_at = Column(DateTime(timezone=True), server_default=func.now())
- started_at = Column(DateTime(timezone=True), nullable=True)
- completed_at = Column(DateTime(timezone=True), nullable=True)
-
- poll = relationship("Poll", back_populates="runs")
- creator = relationship("User", backref="created_poll_runs")
-
-
-class PollResponse(Base):
- __tablename__ = "poll_responses"
- __table_args__ = (
- UniqueConstraint("poll_id", "user_id", name="uq_poll_user"),
- )
-
- id = Column(Integer, primary_key=True, index=True)
- poll_id = Column(Integer, ForeignKey("polls.id", ondelete="CASCADE"), nullable=False)
- run_id = Column(Integer, ForeignKey("poll_runs.id", ondelete="SET NULL"), nullable=True)
- user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
- current_question_id = Column(Integer, ForeignKey("poll_questions.id", ondelete="SET NULL"), nullable=True)
- message_id = Column(Integer, nullable=True)
- chat_id = Column(BigInteger, nullable=True)
- is_completed = Column(Boolean, default=False, nullable=False)
- reward_given = Column(Boolean, default=False, nullable=False)
- reward_amount_kopeks = Column(Integer, default=0, nullable=False)
- created_at = Column(DateTime(timezone=True), server_default=func.now())
- completed_at = Column(DateTime(timezone=True), nullable=True)
-
- poll = relationship("Poll", back_populates="responses")
- run = relationship("PollRun", backref="responses")
- user = relationship("User", backref="poll_responses")
- current_question = relationship("PollQuestion")
- answers = relationship(
- "PollAnswer",
- back_populates="response",
- cascade="all, delete-orphan",
- )
-
-
-class PollAnswer(Base):
- __tablename__ = "poll_answers"
-
- id = Column(Integer, primary_key=True, index=True)
- response_id = Column(Integer, ForeignKey("poll_responses.id", ondelete="CASCADE"), nullable=False)
- question_id = Column(Integer, ForeignKey("poll_questions.id", ondelete="CASCADE"), nullable=False)
- option_id = Column(Integer, ForeignKey("poll_options.id", ondelete="CASCADE"), nullable=False)
- created_at = Column(DateTime(timezone=True), server_default=func.now())
-
- response = relationship("PollResponse", back_populates="answers")
- question = relationship("PollQuestion")
- option = relationship("PollOption")
-
class ServerSquad(Base):
__tablename__ = "server_squads"
diff --git a/app/handlers/admin/polls.py b/app/handlers/admin/polls.py
deleted file mode 100644
index d7130a54..00000000
--- a/app/handlers/admin/polls.py
+++ /dev/null
@@ -1,1259 +0,0 @@
-import asyncio
-import html
-import logging
-from datetime import datetime
-from decimal import Decimal, InvalidOperation
-from typing import List
-
-from aiogram import Bot, Dispatcher, F, types
-from aiogram.fsm.context import FSMContext
-from sqlalchemy import select, func
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import selectinload
-
-from app.database.models import (
- Poll,
- PollAnswer,
- PollOption,
- PollQuestion,
- PollResponse,
- PollRun,
- User,
-)
-from app.handlers.admin.messages import get_target_name, get_target_users
-from app.localization.texts import get_texts
-from app.states import AdminStates
-from app.utils.decorators import admin_required, error_handler
-
-logger = logging.getLogger(__name__)
-
-
-def _format_question_summary(index: int, question: PollQuestion) -> str:
- escaped_question = html.escape(question.text)
- lines = [f"{index}. {escaped_question}"]
- for opt_index, option in enumerate(sorted(question.options, key=lambda o: o.order), start=1):
- lines.append(f" {opt_index}) {html.escape(option.text)}")
- return "\n".join(lines)
-
-
-async def _get_poll(db: AsyncSession, poll_id: int) -> Poll | None:
- stmt = (
- select(Poll)
- .options(
- selectinload(Poll.questions).selectinload(PollQuestion.options),
- selectinload(Poll.runs),
- )
- .where(Poll.id == poll_id)
- )
- result = await db.execute(stmt)
- return result.unique().scalar_one_or_none()
-
-
-def _get_state_questions(data: dict) -> List[dict]:
- return list(data.get("poll_questions", []))
-
-
-def _ensure_questions_present(questions: List[dict]) -> None:
- if not questions:
- raise ValueError("poll_without_questions")
-
-
-@admin_required
-@error_handler
-async def show_polls_menu(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
-
- stmt = (
- select(Poll)
- .options(selectinload(Poll.questions))
- .order_by(Poll.created_at.desc())
- )
- result = await db.execute(stmt)
- polls = result.unique().scalars().all()
-
- text = (
- texts.t("ADMIN_POLLS_TITLE", "📋 Опросы")
- + "\n\n"
- + texts.t(
- "ADMIN_POLLS_DESCRIPTION",
- "Создавайте опросы и отправляйте их пользователям по категориям рассылок.",
- )
- )
-
- keyboard: list[list[types.InlineKeyboardButton]] = []
- for poll in polls:
- question_count = len(poll.questions)
- reward_label = (
- texts.t("ADMIN_POLLS_REWARD_ENABLED", "🎁 награда есть")
- if poll.reward_enabled and poll.reward_amount_kopeks > 0
- else texts.t("ADMIN_POLLS_REWARD_DISABLED", "без награды")
- )
- button_text = f"📋 {poll.title} ({question_count}) — {reward_label}"
- keyboard.append(
- [
- types.InlineKeyboardButton(
- text=button_text,
- callback_data=f"admin_poll_{poll.id}",
- )
- ]
- )
-
- keyboard.append(
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_CREATE", "➕ Создать опрос"),
- callback_data="admin_poll_create",
- )
- ]
- )
- keyboard.append(
- [
- types.InlineKeyboardButton(
- text=texts.BACK,
- callback_data="admin_submenu_communications",
- )
- ]
- )
-
- await callback.message.edit_text(
- text,
- reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def start_poll_creation(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- await state.set_state(AdminStates.creating_poll_title)
- await state.update_data(
- poll_questions=[],
- reward_enabled=False,
- reward_amount_kopeks=0,
- )
-
- await callback.message.edit_text(
- texts.t(
- "ADMIN_POLLS_ENTER_TITLE",
- "🆕 Создание опроса\n\nВведите заголовок опроса.",
- ),
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def process_poll_title(
- message: types.Message,
- db_user: User,
- state: FSMContext,
-):
- title = (message.text or "").strip()
- texts = get_texts(db_user.language)
-
- if not title:
- await message.answer(
- texts.t("ADMIN_POLLS_ENTER_TITLE_RETRY", "❗️ Укажите непустой заголовок."),
- )
- return
-
- await state.update_data(poll_title=title)
- await state.set_state(AdminStates.creating_poll_description)
- await message.answer(
- texts.t(
- "ADMIN_POLLS_ENTER_DESCRIPTION",
- "✍️ Введите описание опроса. HTML-разметка поддерживается.",
- )
- )
-
-
-@admin_required
-@error_handler
-async def process_poll_description(
- message: types.Message,
- db_user: User,
- state: FSMContext,
-):
- description = message.html_text or message.text or ""
- description = description.strip()
- texts = get_texts(db_user.language)
-
- if not description:
- await message.answer(
- texts.t("ADMIN_POLLS_ENTER_DESCRIPTION_RETRY", "❗️ Описание не может быть пустым."),
- )
- return
-
- await state.update_data(poll_description=description)
- await state.set_state(AdminStates.creating_poll_question_text)
- await message.answer(
- texts.t(
- "ADMIN_POLLS_ENTER_QUESTION",
- "❓ Отправьте текст первого вопроса опроса.",
- )
- )
-
-
-@admin_required
-@error_handler
-async def process_poll_question_text(
- message: types.Message,
- db_user: User,
- state: FSMContext,
-):
- question_text = (message.html_text or message.text or "").strip()
- texts = get_texts(db_user.language)
-
- if not question_text:
- await message.answer(
- texts.t(
- "ADMIN_POLLS_ENTER_QUESTION_RETRY",
- "❗️ Текст вопроса не может быть пустым. Отправьте вопрос ещё раз.",
- )
- )
- return
-
- await state.update_data(current_question_text=question_text)
- await state.set_state(AdminStates.creating_poll_question_options)
- await message.answer(
- texts.t(
- "ADMIN_POLLS_ENTER_OPTIONS",
- "🔢 Отправьте варианты ответов, каждый с новой строки (минимум 2, максимум 10).",
- )
- )
-
-
-@admin_required
-@error_handler
-async def process_poll_question_options(
- message: types.Message,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- raw_options = (message.text or "").splitlines()
- options = [opt.strip() for opt in raw_options if opt.strip()]
-
- if len(options) < 2:
- await message.answer(
- texts.t(
- "ADMIN_POLLS_NEED_MORE_OPTIONS",
- "❗️ Укажите минимум два варианта ответа.",
- )
- )
- return
-
- if len(options) > 10:
- await message.answer(
- texts.t(
- "ADMIN_POLLS_TOO_MANY_OPTIONS",
- "❗️ Максимум 10 вариантов ответа. Отправьте список ещё раз.",
- )
- )
- return
-
- data = await state.get_data()
- question_text = data.get("current_question_text")
- if not question_text:
- await message.answer(
- texts.t(
- "ADMIN_POLLS_QUESTION_NOT_FOUND",
- "❌ Не удалось найти текст вопроса. Начните заново, выбрав создание вопроса.",
- )
- )
- await state.set_state(AdminStates.creating_poll_question_text)
- return
-
- questions = _get_state_questions(data)
- questions.append({"text": question_text, "options": options})
- await state.update_data(
- poll_questions=questions,
- current_question_text=None,
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_ADD_QUESTION", "➕ Добавить ещё вопрос"),
- callback_data="admin_poll_add_question",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_CONFIGURE_REWARD", "🎁 Настроить награду"),
- callback_data="admin_poll_reward_menu",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_CANCEL", "❌ Отмена"),
- callback_data="admin_polls",
- )
- ],
- ]
- )
-
- await state.set_state(None)
- await message.answer(
- texts.t(
- "ADMIN_POLLS_QUESTION_ADDED",
- "✅ Вопрос добавлен. Выберите действие:",
- ),
- reply_markup=keyboard,
- )
-
-
-@admin_required
-@error_handler
-async def add_another_question(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- await state.set_state(AdminStates.creating_poll_question_text)
- await callback.message.edit_text(
- texts.t(
- "ADMIN_POLLS_ENTER_QUESTION_NEXT",
- "❓ Отправьте текст следующего вопроса.",
- )
- )
- await callback.answer()
-
-
-def _build_reward_menu(texts, data: dict) -> tuple[str, types.InlineKeyboardMarkup]:
- reward_enabled = bool(data.get("reward_enabled"))
- reward_amount = int(data.get("reward_amount_kopeks") or 0)
- questions = _get_state_questions(data)
-
- questions_summary = "\n".join(
- f"{idx}. {html.escape(q['text'])}" for idx, q in enumerate(questions, start=1)
- ) or texts.t("ADMIN_POLLS_NO_QUESTIONS", "— вопросы не добавлены —")
-
- reward_text = (
- texts.t("ADMIN_POLLS_REWARD_ON", "Включена")
- if reward_enabled and reward_amount > 0
- else texts.t("ADMIN_POLLS_REWARD_OFF", "Отключена")
- )
- reward_amount_label = texts.format_price(reward_amount)
-
- text = (
- texts.t("ADMIN_POLLS_REWARD_TITLE", "🎁 Награда за участие")
- + "\n\n"
- + texts.t("ADMIN_POLLS_REWARD_STATUS", "Статус: {status}" ).format(status=reward_text)
- + "\n"
- + texts.t(
- "ADMIN_POLLS_REWARD_AMOUNT",
- "Сумма: {amount}",
- ).format(amount=reward_amount_label)
- + "\n\n"
- + texts.t("ADMIN_POLLS_REWARD_QUESTIONS", "Всего вопросов: {count}").format(count=len(questions))
- + "\n"
- + questions_summary
- )
-
- toggle_text = (
- texts.t("ADMIN_POLLS_REWARD_DISABLE", "🚫 Отключить награду")
- if reward_enabled
- else texts.t("ADMIN_POLLS_REWARD_ENABLE", "🔔 Включить награду")
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=toggle_text,
- callback_data="admin_poll_toggle_reward",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_REWARD_SET_AMOUNT", "💰 Изменить сумму"),
- callback_data="admin_poll_reward_amount",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_SAVE", "✅ Сохранить опрос"),
- callback_data="admin_poll_save",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_ADD_MORE", "➕ Добавить ещё вопрос"),
- callback_data="admin_poll_add_question",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_CANCEL", "❌ Отмена"),
- callback_data="admin_polls",
- )
- ],
- ]
- )
-
- return text, keyboard
-
-
-@admin_required
-@error_handler
-async def show_reward_menu(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- data = await state.get_data()
- texts = get_texts(db_user.language)
- try:
- _ensure_questions_present(_get_state_questions(data))
- except ValueError:
- await callback.answer(
- texts.t(
- "ADMIN_POLLS_NEED_QUESTION_FIRST",
- "Добавьте хотя бы один вопрос перед настройкой награды.",
- ),
- show_alert=True,
- )
- return
-
- text, keyboard = _build_reward_menu(texts, data)
- await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def toggle_reward(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- data = await state.get_data()
- reward_enabled = bool(data.get("reward_enabled"))
- reward_amount = int(data.get("reward_amount_kopeks") or 0)
-
- reward_enabled = not reward_enabled
- if reward_enabled and reward_amount <= 0:
- reward_amount = 1000
-
- await state.update_data(
- reward_enabled=reward_enabled,
- reward_amount_kopeks=reward_amount,
- )
-
- texts = get_texts(db_user.language)
- text, keyboard = _build_reward_menu(texts, await state.get_data())
- await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def request_reward_amount(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- await state.set_state(AdminStates.creating_poll_reward_amount)
- await callback.message.edit_text(
- texts.t(
- "ADMIN_POLLS_REWARD_AMOUNT_PROMPT",
- "💰 Введите сумму награды в рублях (можно с копейками).",
- )
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def process_reward_amount(
- message: types.Message,
- db_user: User,
- state: FSMContext,
-):
- texts = get_texts(db_user.language)
- raw_value = (message.text or "").replace(",", ".").strip()
-
- try:
- value_decimal = Decimal(raw_value)
- except (InvalidOperation, ValueError):
- await message.answer(
- texts.t(
- "ADMIN_POLLS_REWARD_AMOUNT_INVALID",
- "❗️ Не удалось распознать число. Введите сумму в формате 10 или 12.5",
- )
- )
- return
-
- if value_decimal < 0:
- await message.answer(
- texts.t(
- "ADMIN_POLLS_REWARD_AMOUNT_NEGATIVE",
- "❗️ Сумма не может быть отрицательной.",
- )
- )
- return
-
- amount_kopeks = int((value_decimal * 100).to_integral_value())
- await state.update_data(
- reward_amount_kopeks=amount_kopeks,
- reward_enabled=amount_kopeks > 0,
- )
- await state.set_state(None)
-
- data = await state.get_data()
- text, keyboard = _build_reward_menu(texts, data)
- await message.answer(text, reply_markup=keyboard, parse_mode="HTML")
-
-
-@admin_required
-@error_handler
-async def save_poll(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
- db: AsyncSession,
-):
- data = await state.get_data()
- texts = get_texts(db_user.language)
-
- try:
- _ensure_questions_present(_get_state_questions(data))
- except ValueError:
- await callback.answer(
- texts.t(
- "ADMIN_POLLS_NEED_QUESTION_FIRST",
- "Добавьте хотя бы один вопрос перед сохранением.",
- ),
- show_alert=True,
- )
- return
-
- title = data.get("poll_title")
- description = data.get("poll_description")
- questions = _get_state_questions(data)
- reward_enabled = bool(data.get("reward_enabled"))
- reward_amount = int(data.get("reward_amount_kopeks") or 0)
-
- if not title or not description:
- await callback.answer(
- texts.t(
- "ADMIN_POLLS_MISSING_DATA",
- "Заполните заголовок и описание перед сохранением.",
- ),
- show_alert=True,
- )
- return
-
- poll = Poll(
- title=title,
- description=description,
- reward_enabled=reward_enabled and reward_amount > 0,
- reward_amount_kopeks=reward_amount if reward_amount > 0 else 0,
- created_by=db_user.id,
- created_at=datetime.utcnow(),
- )
-
- try:
- db.add(poll)
- await db.flush()
-
- for q_index, question_data in enumerate(questions, start=1):
- question = PollQuestion(
- poll_id=poll.id,
- text=question_data["text"],
- order=q_index,
- )
- db.add(question)
- await db.flush()
-
- for opt_index, option_text in enumerate(question_data["options"], start=1):
- option = PollOption(
- question_id=question.id,
- text=option_text,
- order=opt_index,
- )
- db.add(option)
-
- await db.commit()
- await state.clear()
-
- poll = await _get_poll(db, poll.id)
- question_lines = [
- _format_question_summary(idx, question)
- for idx, question in enumerate(poll.questions, start=1)
- ]
- reward_info = (
- texts.t(
- "ADMIN_POLLS_REWARD_SUMMARY",
- "🎁 Награда: {amount}",
- ).format(amount=texts.format_price(poll.reward_amount_kopeks))
- if poll.reward_enabled and poll.reward_amount_kopeks > 0
- else texts.t("ADMIN_POLLS_REWARD_SUMMARY_NONE", "🎁 Награда: не выдается")
- )
-
- summary_text = (
- texts.t("ADMIN_POLLS_CREATED", "✅ Опрос сохранён!")
- + "\n\n"
- + f"{html.escape(poll.title)}\n"
- + texts.t("ADMIN_POLLS_QUESTIONS_COUNT", "Вопросов: {count}").format(count=len(poll.questions))
- + "\n"
- + reward_info
- + "\n\n"
- + "\n".join(question_lines)
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_OPEN", "📋 К опросу"),
- callback_data=f"admin_poll_{poll.id}",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.BACK,
- callback_data="admin_polls",
- )
- ],
- ]
- )
-
- await callback.message.edit_text(
- summary_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- await callback.answer()
-
- except Exception as exc: # pragma: no cover - defensive logging
- await db.rollback()
- logger.exception("Failed to create poll: %s", exc)
- await callback.answer(
- texts.t(
- "ADMIN_POLLS_SAVE_ERROR",
- "❌ Не удалось сохранить опрос. Попробуйте ещё раз позже.",
- ),
- show_alert=True,
- )
-
-
-@admin_required
-@error_handler
-async def show_poll_details(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- try:
- poll_id = int(callback.data.split("_")[-1])
- except (ValueError, IndexError):
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- poll = await _get_poll(db, poll_id)
- if not poll:
- await callback.answer(
- texts.t("ADMIN_POLLS_NOT_FOUND", "Опрос не найден или был удалён."),
- show_alert=True,
- )
- return
-
- question_lines = [
- _format_question_summary(idx, question)
- for idx, question in enumerate(poll.questions, start=1)
- ]
-
- runs_total = sum(run.sent_count for run in poll.runs)
- completions = await db.scalar(
- select(func.count(PollResponse.id)).where(
- PollResponse.poll_id == poll.id,
- PollResponse.is_completed.is_(True),
- )
- ) or 0
-
- reward_info = (
- texts.t(
- "ADMIN_POLLS_REWARD_SUMMARY",
- "🎁 Награда: {amount}",
- ).format(amount=texts.format_price(poll.reward_amount_kopeks))
- if poll.reward_enabled and poll.reward_amount_kopeks > 0
- else texts.t("ADMIN_POLLS_REWARD_SUMMARY_NONE", "🎁 Награда: не выдается")
- )
-
- description_preview = html.escape(poll.description)
-
- text = (
- f"📋 {html.escape(poll.title)}\n\n"
- + texts.t("ADMIN_POLLS_DESCRIPTION_LABEL", "Описание:")
- + f"\n{description_preview}\n\n"
- + texts.t(
- "ADMIN_POLLS_STATS_SENT",
- "Отправлено сообщений: {count}",
- ).format(count=runs_total)
- + "\n"
- + texts.t(
- "ADMIN_POLLS_STATS_COMPLETED",
- "Завершили опрос: {count}",
- ).format(count=completions)
- + "\n"
- + reward_info
- + "\n\n"
- + texts.t("ADMIN_POLLS_QUESTIONS_LIST", "Вопросы:")
- + "\n"
- + ("\n".join(question_lines) if question_lines else texts.t("ADMIN_POLLS_NO_QUESTIONS", "— вопросы не добавлены —"))
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_SEND", "🚀 Отправить"),
- callback_data=f"admin_poll_send_{poll.id}",
- ),
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_STATS_BUTTON", "📊 Статистика"),
- callback_data=f"admin_poll_stats_{poll.id}",
- ),
- ],
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_DELETE", "🗑️ Удалить"),
- callback_data=f"admin_poll_delete_{poll.id}",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.BACK,
- callback_data="admin_polls",
- )
- ],
- ]
- )
-
- await callback.message.edit_text(
- text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def show_poll_target_selection(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- try:
- poll_id = int(callback.data.split("_")[-1])
- except (ValueError, IndexError):
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- poll = await _get_poll(db, poll_id)
- if not poll or not poll.questions:
- await callback.answer(
- texts.t(
- "ADMIN_POLLS_NO_QUESTIONS",
- "Сначала добавьте вопросы к опросу, чтобы отправлять его пользователям.",
- ),
- show_alert=True,
- )
- return
-
- from app.keyboards.admin import get_poll_target_keyboard
-
- await callback.message.edit_text(
- texts.t(
- "ADMIN_POLLS_SELECT_TARGET",
- "🎯 Выберите категорию пользователей для отправки опроса.",
- ),
- reply_markup=get_poll_target_keyboard(poll.id, db_user.language),
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def preview_poll_target(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- try:
- _, _, poll_id_str, target = callback.data.split("_", 3)
- poll_id = int(poll_id_str)
- except (ValueError, IndexError):
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- poll = await _get_poll(db, poll_id)
- if not poll:
- await callback.answer(
- texts.t("ADMIN_POLLS_NOT_FOUND", "Опрос не найден."),
- show_alert=True,
- )
- return
-
- users = await get_target_users(db, target)
- target_name = get_target_name(target)
-
- confirm_keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_CONFIRM_SEND", "✅ Отправить"),
- callback_data=f"admin_poll_send_confirm_{poll_id}_{target}",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.BACK,
- callback_data=f"admin_poll_send_{poll_id}",
- )
- ],
- ]
- )
-
- text = (
- texts.t("ADMIN_POLLS_CONFIRMATION_TITLE", "📨 Подтверждение отправки")
- + "\n\n"
- + texts.t(
- "ADMIN_POLLS_CONFIRMATION_BODY",
- "Категория: {category}\nПользователей: {count}",
- ).format(category=target_name, count=len(users))
- + "\n\n"
- + texts.t(
- "ADMIN_POLLS_CONFIRMATION_HINT",
- "После отправки пользователи получат приглашение пройти опрос.",
- )
- )
-
- await callback.message.edit_text(
- text,
- reply_markup=confirm_keyboard,
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-async def _send_poll_invitation(
- bot: Bot,
- poll: Poll,
- run: PollRun,
- users: list,
-) -> tuple[int, int]:
- sent_count = 0
- failed_count = 0
-
- invite_text = (
- f"📋 {html.escape(poll.title)}\n\n"
- f"{poll.description}\n\n"
- "📝 Нажмите кнопку ниже, чтобы пройти опрос."
- )
-
- for index, user in enumerate(users, start=1):
- try:
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text="📝 Пройти опрос",
- callback_data=f"poll_start_{poll.id}_{run.id}",
- )
- ]
- ]
- )
- await bot.send_message(
- chat_id=user.telegram_id,
- text=invite_text,
- reply_markup=keyboard,
- )
- sent_count += 1
- except Exception as exc: # pragma: no cover - defensive logging
- failed_count += 1
- logger.warning(
- "Failed to send poll %s to user %s: %s",
- poll.id,
- getattr(user, "telegram_id", "unknown"),
- exc,
- )
- if index % 25 == 0:
- await asyncio.sleep(0.5)
-
- return sent_count, failed_count
-
-
-@admin_required
-@error_handler
-async def confirm_poll_sending(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- parts = callback.data.split("_")
- if len(parts) < 6:
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- try:
- poll_id = int(parts[4])
- except ValueError:
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- target = "_".join(parts[5:])
-
- poll = await _get_poll(db, poll_id)
- if not poll:
- await callback.answer(
- texts.t("ADMIN_POLLS_NOT_FOUND", "Опрос не найден."),
- show_alert=True,
- )
- return
-
- users = await get_target_users(db, target)
- if not users:
- await callback.answer(
- texts.t(
- "ADMIN_POLLS_NO_USERS",
- "Подходящих пользователей не найдено для выбранной категории.",
- ),
- show_alert=True,
- )
- return
-
- await callback.message.edit_text(
- texts.t("ADMIN_POLLS_SENDING", "📨 Отправляем опрос..."),
- )
-
- run = PollRun(
- poll_id=poll.id,
- target_type=target,
- status="in_progress",
- total_count=len(users),
- created_by=db_user.id,
- created_at=datetime.utcnow(),
- started_at=datetime.utcnow(),
- )
- db.add(run)
- await db.flush()
-
- sent_count, failed_count = await _send_poll_invitation(callback.bot, poll, run, users)
-
- run.sent_count = sent_count
- run.failed_count = failed_count
- run.status = "completed"
- run.completed_at = datetime.utcnow()
-
- await db.commit()
-
- result_text = (
- texts.t("ADMIN_POLLS_SENT", "✅ Отправка завершена!")
- + "\n\n"
- + texts.t("ADMIN_POLLS_SENT_SUCCESS", "Успешно отправлено: {count}").format(count=sent_count)
- + "\n"
- + texts.t("ADMIN_POLLS_SENT_FAILED", "Ошибок доставки: {count}").format(count=failed_count)
- + "\n"
- + texts.t("ADMIN_POLLS_SENT_TOTAL", "Всего пользователей: {count}").format(count=len(users))
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_STATS_BUTTON", "📊 Статистика"),
- callback_data=f"admin_poll_stats_{poll.id}",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.BACK,
- callback_data=f"admin_poll_{poll.id}",
- )
- ],
- ]
- )
-
- await callback.message.edit_text(
- result_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def show_poll_stats(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- try:
- poll_id = int(callback.data.split("_")[-1])
- except (ValueError, IndexError):
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- poll = await _get_poll(db, poll_id)
- if not poll:
- await callback.answer(
- texts.t("ADMIN_POLLS_NOT_FOUND", "Опрос не найден."),
- show_alert=True,
- )
- return
-
- total_responses = await db.scalar(
- select(func.count(PollResponse.id)).where(PollResponse.poll_id == poll.id)
- ) or 0
- completed_responses = await db.scalar(
- select(func.count(PollResponse.id)).where(
- PollResponse.poll_id == poll.id,
- PollResponse.is_completed.is_(True),
- )
- ) or 0
- reward_sum = await db.scalar(
- select(func.coalesce(func.sum(PollResponse.reward_amount_kopeks), 0)).where(
- PollResponse.poll_id == poll.id,
- PollResponse.reward_given.is_(True),
- )
- ) or 0
-
- runs_total = await db.scalar(
- select(func.coalesce(func.sum(PollRun.sent_count), 0)).where(PollRun.poll_id == poll.id)
- ) or 0
-
- answers_stmt = (
- select(PollAnswer.question_id, PollAnswer.option_id, func.count(PollAnswer.id))
- .join(PollResponse, PollResponse.id == PollAnswer.response_id)
- .where(PollResponse.poll_id == poll.id)
- .group_by(PollAnswer.question_id, PollAnswer.option_id)
- )
- answers_result = await db.execute(answers_stmt)
- answer_counts = {
- (question_id, option_id): count
- for question_id, option_id, count in answers_result.all()
- }
-
- question_lines = []
- for question in sorted(poll.questions, key=lambda q: q.order):
- total_answers_for_question = sum(
- answer_counts.get((question.id, option.id), 0)
- for option in question.options
- ) or 0
- question_lines.append(f"{html.escape(question.text)}")
- for option in sorted(question.options, key=lambda o: o.order):
- option_count = answer_counts.get((question.id, option.id), 0)
- percent = (
- round(option_count / total_answers_for_question * 100, 1)
- if total_answers_for_question
- else 0
- )
- question_lines.append(
- texts.t(
- "ADMIN_POLLS_STATS_OPTION",
- "• {text} — {count} ({percent}%)",
- ).format(
- text=html.escape(option.text),
- count=option_count,
- percent=percent,
- )
- )
- question_lines.append("")
-
- text = (
- texts.t("ADMIN_POLLS_STATS_TITLE", "📊 Статистика опроса")
- + "\n\n"
- + f"{html.escape(poll.title)}\n"
- + texts.t("ADMIN_POLLS_STATS_SENT", "Сообщений отправлено: {count}").format(count=runs_total)
- + "\n"
- + texts.t(
- "ADMIN_POLLS_STATS_RESPONDED",
- "Ответов получено: {count}",
- ).format(count=total_responses)
- + "\n"
- + texts.t(
- "ADMIN_POLLS_STATS_COMPLETED_LABEL",
- "Прошли до конца: {count}",
- ).format(count=completed_responses)
- + "\n"
- + texts.t(
- "ADMIN_POLLS_STATS_REWARD_TOTAL",
- "Выдано наград: {amount}",
- ).format(amount=texts.format_price(reward_sum))
- + "\n\n"
- + ("\n".join(question_lines).strip() or texts.t("ADMIN_POLLS_STATS_NO_DATA", "Ответов пока нет."))
- )
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.BACK,
- callback_data=f"admin_poll_{poll.id}",
- )
- ],
- ]
- )
-
- await callback.message.edit_text(
- text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def ask_delete_poll(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- try:
- poll_id = int(callback.data.split("_")[-1])
- except (ValueError, IndexError):
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- poll = await _get_poll(db, poll_id)
- if not poll:
- await callback.answer(
- texts.t("ADMIN_POLLS_NOT_FOUND", "Опрос не найден."),
- show_alert=True,
- )
- return
-
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_DELETE_CONFIRM", "🗑️ Удалить"),
- callback_data=f"admin_poll_delete_confirm_{poll.id}",
- )
- ],
- [
- types.InlineKeyboardButton(
- text=texts.BACK,
- callback_data=f"admin_poll_{poll.id}",
- )
- ],
- ]
- )
-
- await callback.message.edit_text(
- texts.t(
- "ADMIN_POLLS_DELETE_PROMPT",
- "❓ Удалить опрос? Это действие нельзя отменить.",
- ),
- reply_markup=keyboard,
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def delete_poll(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- try:
- poll_id = int(callback.data.split("_")[-1])
- except (ValueError, IndexError):
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- poll = await db.get(Poll, poll_id)
- if not poll:
- await callback.answer(
- texts.t("ADMIN_POLLS_NOT_FOUND", "Опрос уже удалён."),
- show_alert=True,
- )
- return
-
- await db.delete(poll)
- await db.commit()
-
- await callback.message.edit_text(
- texts.t("ADMIN_POLLS_DELETED", "🗑️ Опрос удалён."),
- reply_markup=types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.t("ADMIN_POLLS_BACK_TO_LIST", "⬅️ К списку опросов"),
- callback_data="admin_polls",
- )
- ]
- ]
- ),
- )
- await callback.answer()
-
-
-def register_handlers(dp: Dispatcher) -> None:
- dp.callback_query.register(show_polls_menu, F.data == "admin_polls")
- dp.callback_query.register(start_poll_creation, F.data == "admin_poll_create")
- dp.message.register(process_poll_title, AdminStates.creating_poll_title)
- dp.message.register(process_poll_description, AdminStates.creating_poll_description)
- dp.message.register(process_poll_question_text, AdminStates.creating_poll_question_text)
- dp.message.register(process_poll_question_options, AdminStates.creating_poll_question_options)
- dp.message.register(process_reward_amount, AdminStates.creating_poll_reward_amount)
-
- dp.callback_query.register(add_another_question, F.data == "admin_poll_add_question")
- dp.callback_query.register(show_reward_menu, F.data == "admin_poll_reward_menu")
- dp.callback_query.register(toggle_reward, F.data == "admin_poll_toggle_reward")
- dp.callback_query.register(request_reward_amount, F.data == "admin_poll_reward_amount")
- dp.callback_query.register(save_poll, F.data == "admin_poll_save")
-
- dp.callback_query.register(
- show_poll_details,
- F.data.regexp(r"^admin_poll_(?!send_|stats_|delete_|create).+"),
- )
- dp.callback_query.register(
- show_poll_target_selection,
- F.data.regexp(r"^admin_poll_send_\\d+$"),
- )
- dp.callback_query.register(preview_poll_target, F.data.startswith("poll_target_"))
- dp.callback_query.register(
- confirm_poll_sending,
- F.data.regexp(r"^admin_poll_send_confirm_\\d+_.+"),
- )
- dp.callback_query.register(
- show_poll_stats,
- F.data.regexp(r"^admin_poll_stats_\\d+$"),
- )
- dp.callback_query.register(
- ask_delete_poll,
- F.data.regexp(r"^admin_poll_delete_\\d+$"),
- )
- dp.callback_query.register(
- delete_poll,
- F.data.regexp(r"^admin_poll_delete_confirm_\\d+$"),
- )
diff --git a/app/handlers/polls.py b/app/handlers/polls.py
deleted file mode 100644
index 491519ca..00000000
--- a/app/handlers/polls.py
+++ /dev/null
@@ -1,322 +0,0 @@
-import asyncio
-import html
-import logging
-from datetime import datetime
-
-from aiogram import Bot, Dispatcher, F, types
-from aiogram.exceptions import TelegramBadRequest
-from sqlalchemy import select
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import selectinload
-
-from app.database.crud.user import add_user_balance
-from app.database.models import (
- Poll,
- PollAnswer,
- PollOption,
- PollQuestion,
- PollResponse,
- PollRun,
- User,
-)
-from app.localization.texts import get_texts
-from app.utils.decorators import error_handler
-
-logger = logging.getLogger(__name__)
-
-
-async def _get_poll_with_questions(db: AsyncSession, poll_id: int) -> Poll | None:
- stmt = (
- select(Poll)
- .options(selectinload(Poll.questions).selectinload(PollQuestion.options))
- .where(Poll.id == poll_id)
- )
- result = await db.execute(stmt)
- return result.unique().scalar_one_or_none()
-
-
-async def _get_response(
- db: AsyncSession,
- poll_id: int,
- user_id: int,
-) -> PollResponse | None:
- stmt = (
- select(PollResponse)
- .options(selectinload(PollResponse.answers))
- .where(PollResponse.poll_id == poll_id, PollResponse.user_id == user_id)
- )
- result = await db.execute(stmt)
- return result.unique().scalar_one_or_none()
-
-
-def _get_next_question(
- poll: Poll,
- response: PollResponse,
-) -> PollQuestion | None:
- questions = sorted(poll.questions, key=lambda q: q.order)
- answered_ids = {answer.question_id for answer in response.answers}
- for question in questions:
- if question.id not in answered_ids:
- return question
- return None
-
-
-async def _delete_message_after_delay(bot: Bot, chat_id: int, message_id: int) -> None:
- await asyncio.sleep(10)
- try:
- await bot.delete_message(chat_id, message_id)
- except TelegramBadRequest:
- pass
- except Exception as exc: # pragma: no cover - defensive logging
- logger.warning("Failed to delete poll message %s: %s", message_id, exc)
-
-
-def _build_question_text(
- poll: Poll,
- question: PollQuestion,
- question_index: int,
- total_questions: int,
- include_description: bool,
-) -> str:
- header = f"📋 {html.escape(poll.title)}\n\n"
- body = ""
- if include_description:
- body += f"{poll.description}\n\n"
- body += (
- f"❓ {question_index}/{total_questions}\n"
- f"{html.escape(question.text)}"
- )
- return header + body
-
-
-def _build_question_keyboard(response_id: int, question: PollQuestion) -> types.InlineKeyboardMarkup:
- buttons = []
- for option in sorted(question.options, key=lambda o: o.order):
- buttons.append(
- [
- types.InlineKeyboardButton(
- text=option.text,
- callback_data=f"poll_answer_{response_id}_{question.id}_{option.id}",
- )
- ]
- )
- return types.InlineKeyboardMarkup(inline_keyboard=buttons)
-
-
-@error_handler
-async def start_poll(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- try:
- _, poll_id_str, run_id_str = callback.data.split("_", 2)
- poll_id = int(poll_id_str)
- run_id = int(run_id_str)
- except (ValueError, IndexError):
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- poll = await _get_poll_with_questions(db, poll_id)
- if not poll or not poll.questions:
- await callback.answer(
- texts.t("POLL_NOT_AVAILABLE", "Опрос недоступен."),
- show_alert=True,
- )
- return
-
- response = await _get_response(db, poll.id, db_user.id)
- if response and response.is_completed:
- await callback.answer(
- texts.t("POLL_ALREADY_PASSED", "Вы уже участвовали в этом опросе."),
- show_alert=True,
- )
- return
-
- if not response:
- response = PollResponse(
- poll_id=poll.id,
- run_id=run_id,
- user_id=db_user.id,
- message_id=callback.message.message_id,
- chat_id=callback.message.chat.id,
- created_at=datetime.utcnow(),
- )
- db.add(response)
- await db.flush()
- else:
- response.message_id = callback.message.message_id
- response.chat_id = callback.message.chat.id
- await db.flush()
-
- next_question = _get_next_question(poll, response)
- if not next_question:
- await callback.answer(
- texts.t("POLL_NO_QUESTIONS", "Вопросы опроса не найдены."),
- show_alert=True,
- )
- return
-
- response.current_question_id = next_question.id
- await db.commit()
-
- question_index = sorted(poll.questions, key=lambda q: q.order).index(next_question) + 1
- total_questions = len(poll.questions)
- include_description = len(response.answers) == 0
- text = _build_question_text(poll, next_question, question_index, total_questions, include_description)
- keyboard = _build_question_keyboard(response.id, next_question)
-
- try:
- await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
- new_message = None
- except TelegramBadRequest:
- new_message = await callback.message.answer(text, reply_markup=keyboard, parse_mode="HTML")
- if new_message:
- response.message_id = new_message.message_id
- response.chat_id = new_message.chat.id
- await db.commit()
- await callback.answer()
-
-
-@error_handler
-async def answer_poll(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
-):
- texts = get_texts(db_user.language)
- try:
- _, response_id_str, question_id_str, option_id_str = callback.data.split("_", 3)
- response_id = int(response_id_str)
- question_id = int(question_id_str)
- option_id = int(option_id_str)
- except (ValueError, IndexError):
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- response = await db.get(PollResponse, response_id)
- if not response or response.user_id != db_user.id:
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- if response.is_completed:
- await callback.answer(
- texts.t("POLL_ALREADY_PASSED", "Вы уже участвовали в этом опросе."),
- show_alert=True,
- )
- return
-
- poll = await _get_poll_with_questions(db, response.poll_id)
- if not poll:
- await callback.answer(texts.t("POLL_NOT_AVAILABLE", "Опрос недоступен."), show_alert=True)
- return
-
- question = next((q for q in poll.questions if q.id == question_id), None)
- if not question:
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- option = next((opt for opt in question.options if opt.id == option_id), None)
- if not option:
- await callback.answer(texts.UNKNOWN_ERROR, show_alert=True)
- return
-
- existing_answer = next((ans for ans in response.answers if ans.question_id == question_id), None)
- if existing_answer:
- await callback.answer(texts.t("POLL_OPTION_ALREADY_CHOSEN", "Ответ уже выбран."))
- return
-
- answer = PollAnswer(
- response_id=response.id,
- question_id=question.id,
- option_id=option.id,
- )
- db.add(answer)
- await db.flush()
-
- response.answers.append(answer)
-
- next_question = _get_next_question(poll, response)
-
- if next_question:
- response.current_question_id = next_question.id
- await db.commit()
-
- question_index = sorted(poll.questions, key=lambda q: q.order).index(next_question) + 1
- total_questions = len(poll.questions)
- include_description = False
- text = _build_question_text(poll, next_question, question_index, total_questions, include_description)
- keyboard = _build_question_keyboard(response.id, next_question)
-
- try:
- await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
- new_message = None
- except TelegramBadRequest:
- new_message = await callback.message.answer(text, reply_markup=keyboard, parse_mode="HTML")
- if new_message:
- response.message_id = new_message.message_id
- response.chat_id = new_message.chat.id
- await db.commit()
- await callback.answer()
- return
-
- # Completed
- response.current_question_id = None
- response.is_completed = True
- response.completed_at = datetime.utcnow()
-
- reward_text = ""
- if poll.reward_enabled and poll.reward_amount_kopeks > 0 and not response.reward_given:
- success = await add_user_balance(
- db,
- db_user,
- poll.reward_amount_kopeks,
- description=texts.t(
- "POLL_REWARD_DESCRIPTION",
- "Награда за участие в опросе '{title}'",
- ).format(title=poll.title),
- )
- if success:
- response.reward_given = True
- response.reward_amount_kopeks = poll.reward_amount_kopeks
- reward_text = texts.t(
- "POLL_REWARD_RECEIVED",
- "\n\n🎁 На баланс зачислено: {amount}",
- ).format(amount=texts.format_price(poll.reward_amount_kopeks))
- else:
- logger.warning("Failed to add reward for poll %s to user %s", poll.id, db_user.telegram_id)
-
- if response.run_id:
- run = await db.get(PollRun, response.run_id)
- if run:
- run.completed_count = (run.completed_count or 0) + 1
-
- await db.commit()
-
- thank_you_text = (
- texts.t("POLL_COMPLETED", "🙏 Спасибо за участие в опросе!")
- + reward_text
- )
-
- try:
- await callback.message.edit_text(thank_you_text)
- new_message = None
- except TelegramBadRequest:
- new_message = await callback.message.answer(thank_you_text)
- if new_message:
- response.message_id = new_message.message_id
- response.chat_id = new_message.chat.id
- await db.commit()
-
- if response.chat_id and response.message_id:
- asyncio.create_task(
- _delete_message_after_delay(callback.bot, response.chat_id, response.message_id)
- )
-
- await callback.answer()
-
-
-def register_handlers(dp: Dispatcher) -> None:
- dp.callback_query.register(start_poll, F.data.startswith("poll_start_"))
- dp.callback_query.register(answer_poll, F.data.startswith("poll_answer_"))
diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py
index 480dd283..75e72bc0 100644
--- a/app/keyboards/admin.py
+++ b/app/keyboards/admin.py
@@ -96,17 +96,11 @@ def get_admin_promo_submenu_keyboard(language: str = "ru") -> InlineKeyboardMark
def get_admin_communications_submenu_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
-
+
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=texts.ADMIN_MESSAGES, callback_data="admin_messages")
],
- [
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_COMMUNICATIONS_POLLS", "📋 Опросы"),
- callback_data="admin_polls"
- )
- ],
[
InlineKeyboardButton(
text=_t(texts, "ADMIN_COMMUNICATIONS_PROMO_OFFERS", "🎯 Промо-предложения"),
@@ -978,54 +972,6 @@ def get_broadcast_target_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
])
-def get_poll_target_keyboard(poll_id: int, language: str = "ru") -> InlineKeyboardMarkup:
- texts = get_texts(language)
-
- return InlineKeyboardMarkup(inline_keyboard=[
- [
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_BROADCAST_TARGET_ALL", "👥 Всем"),
- callback_data=f"poll_target_{poll_id}_all"
- ),
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_BROADCAST_TARGET_ACTIVE", "📱 С подпиской"),
- callback_data=f"poll_target_{poll_id}_active"
- )
- ],
- [
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_BROADCAST_TARGET_TRIAL", "🎁 Триал"),
- callback_data=f"poll_target_{poll_id}_trial"
- ),
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_BROADCAST_TARGET_NO_SUB", "❌ Без подписки"),
- callback_data=f"poll_target_{poll_id}_no"
- )
- ],
- [
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_BROADCAST_TARGET_EXPIRING", "⏰ Истекающие"),
- callback_data=f"poll_target_{poll_id}_expiring"
- ),
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_BROADCAST_TARGET_EXPIRED", "🔚 Истекшие"),
- callback_data=f"poll_target_{poll_id}_expired"
- )
- ],
- [
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_BROADCAST_TARGET_ACTIVE_ZERO", "🧊 Активна 0 ГБ"),
- callback_data=f"poll_target_{poll_id}_active_zero"
- ),
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_BROADCAST_TARGET_TRIAL_ZERO", "🥶 Триал 0 ГБ"),
- callback_data=f"poll_target_{poll_id}_trial_zero"
- )
- ],
- [InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_poll_{poll_id}")]
- ])
-
-
def get_custom_criteria_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index b9206713..eee72fe3 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -1375,85 +1375,5 @@
"SIMPLE_SUBSCRIPTION_SERVER_ANY": "Any available",
"SIMPLE_SUBSCRIPTION_SERVER_SELECTED": "Selected",
"SIMPLE_SUBSCRIPTION_SERVER_ASSIGNED": "Assigned automatically",
- "MENU_SIMPLE_SUBSCRIPTION": "⚡ Quick purchase",
- "ADMIN_COMMUNICATIONS_POLLS": "📋 Polls",
- "ADMIN_POLLS_TITLE": "📋 Polls",
- "ADMIN_POLLS_DESCRIPTION": "Create polls and send them to users by broadcast categories.",
- "ADMIN_POLLS_REWARD_ENABLED": "reward enabled",
- "ADMIN_POLLS_REWARD_DISABLED": "no reward",
- "ADMIN_POLLS_CREATE": "➕ Create poll",
- "ADMIN_POLLS_ENTER_TITLE": "🆕 Create a poll\n\nSend the poll title.",
- "ADMIN_POLLS_ENTER_TITLE_RETRY": "❗️ Title cannot be empty.",
- "ADMIN_POLLS_ENTER_DESCRIPTION": "✍️ Send the poll description. HTML markup is supported.",
- "ADMIN_POLLS_ENTER_DESCRIPTION_RETRY": "❗️ Description cannot be empty.",
- "ADMIN_POLLS_ENTER_QUESTION": "❓ Send the text of the first question.",
- "ADMIN_POLLS_ENTER_QUESTION_RETRY": "❗️ Question text cannot be empty. Please resend it.",
- "ADMIN_POLLS_ENTER_OPTIONS": "🔢 Send answer options, one per line (min 2, max 10).",
- "ADMIN_POLLS_NEED_MORE_OPTIONS": "❗️ Provide at least two answer options.",
- "ADMIN_POLLS_TOO_MANY_OPTIONS": "❗️ Maximum 10 options. Please send the list again.",
- "ADMIN_POLLS_QUESTION_NOT_FOUND": "❌ Could not find question text. Start again by creating a question.",
- "ADMIN_POLLS_ADD_QUESTION": "➕ Add another question",
- "ADMIN_POLLS_CONFIGURE_REWARD": "🎁 Configure reward",
- "ADMIN_POLLS_CANCEL": "❌ Cancel",
- "ADMIN_POLLS_QUESTION_ADDED": "✅ Question saved. Choose the next action:",
- "ADMIN_POLLS_ENTER_QUESTION_NEXT": "❓ Send the next question text.",
- "ADMIN_POLLS_NO_QUESTIONS": "— no questions yet —",
- "ADMIN_POLLS_REWARD_TITLE": "🎁 Reward settings",
- "ADMIN_POLLS_REWARD_STATUS": "Status: {status}",
- "ADMIN_POLLS_REWARD_AMOUNT": "Amount: {amount}",
- "ADMIN_POLLS_REWARD_QUESTIONS": "Total questions: {count}",
- "ADMIN_POLLS_REWARD_DISABLE": "🚫 Disable reward",
- "ADMIN_POLLS_REWARD_ENABLE": "🔔 Enable reward",
- "ADMIN_POLLS_REWARD_SET_AMOUNT": "💰 Change amount",
- "ADMIN_POLLS_SAVE": "✅ Save poll",
- "ADMIN_POLLS_ADD_MORE": "➕ Add another question",
- "ADMIN_POLLS_NEED_QUESTION_FIRST": "Add at least one question before configuring the reward.",
- "ADMIN_POLLS_REWARD_AMOUNT_PROMPT": "💰 Enter reward amount in RUB (decimals allowed).",
- "ADMIN_POLLS_REWARD_AMOUNT_INVALID": "❗️ Could not parse the number. Use format 10 or 12.5",
- "ADMIN_POLLS_REWARD_AMOUNT_NEGATIVE": "❗️ Amount cannot be negative.",
- "ADMIN_POLLS_MISSING_DATA": "Fill in title and description before saving.",
- "ADMIN_POLLS_REWARD_ON": "Enabled",
- "ADMIN_POLLS_REWARD_OFF": "Disabled",
- "ADMIN_POLLS_REWARD_SUMMARY": "🎁 Reward: {amount}",
- "ADMIN_POLLS_REWARD_SUMMARY_NONE": "🎁 Reward: not provided",
- "ADMIN_POLLS_CREATED": "✅ Poll saved!",
- "ADMIN_POLLS_QUESTIONS_COUNT": "Questions: {count}",
- "ADMIN_POLLS_OPEN": "📋 Open poll",
- "ADMIN_POLLS_SAVE_ERROR": "❌ Failed to save poll. Please try again later.",
- "ADMIN_POLLS_NOT_FOUND": "Poll not found or already removed.",
- "ADMIN_POLLS_DESCRIPTION_LABEL": "Description:",
- "ADMIN_POLLS_STATS_SENT": "Messages sent: {count}",
- "ADMIN_POLLS_STATS_COMPLETED": "Finished the poll: {count}",
- "ADMIN_POLLS_QUESTIONS_LIST": "Questions:",
- "ADMIN_POLLS_SEND": "🚀 Send",
- "ADMIN_POLLS_STATS_BUTTON": "📊 Stats",
- "ADMIN_POLLS_DELETE": "🗑️ Delete",
- "ADMIN_POLLS_SELECT_TARGET": "🎯 Choose a user segment for this poll.",
- "ADMIN_POLLS_CONFIRM_SEND": "✅ Send",
- "ADMIN_POLLS_CONFIRMATION_TITLE": "📨 Send confirmation",
- "ADMIN_POLLS_CONFIRMATION_BODY": "Segment: {category}\nUsers: {count}",
- "ADMIN_POLLS_CONFIRMATION_HINT": "Users will receive an invite to complete the poll.",
- "ADMIN_POLLS_NO_USERS": "No users matched the selected category.",
- "ADMIN_POLLS_SENDING": "📨 Sending the poll...",
- "ADMIN_POLLS_SENT": "✅ Sending completed!",
- "ADMIN_POLLS_SENT_SUCCESS": "Successfully sent: {count}",
- "ADMIN_POLLS_SENT_FAILED": "Failed deliveries: {count}",
- "ADMIN_POLLS_SENT_TOTAL": "Total recipients: {count}",
- "ADMIN_POLLS_STATS_TITLE": "📊 Poll statistics",
- "ADMIN_POLLS_STATS_RESPONDED": "Responses received: {count}",
- "ADMIN_POLLS_STATS_COMPLETED_LABEL": "Completed: {count}",
- "ADMIN_POLLS_STATS_REWARD_TOTAL": "Rewards issued: {amount}",
- "ADMIN_POLLS_STATS_OPTION": "• {text} — {count} ({percent}%)",
- "ADMIN_POLLS_STATS_NO_DATA": "No answers yet.",
- "ADMIN_POLLS_DELETE_CONFIRM": "🗑️ Delete",
- "ADMIN_POLLS_DELETE_PROMPT": "❓ Delete the poll? This action cannot be undone.",
- "ADMIN_POLLS_DELETED": "🗑️ Poll deleted.",
- "ADMIN_POLLS_BACK_TO_LIST": "⬅️ Back to polls list",
- "POLL_NOT_AVAILABLE": "Poll is not available.",
- "POLL_ALREADY_PASSED": "You have already completed this poll.",
- "POLL_NO_QUESTIONS": "No poll questions found.",
- "POLL_OPTION_ALREADY_CHOSEN": "Answer already selected.",
- "POLL_REWARD_DESCRIPTION": "Reward for completing poll '{title}'",
- "POLL_REWARD_RECEIVED": "\n\n🎁 Credited to balance: {amount}",
- "POLL_COMPLETED": "🙏 Thanks for completing the poll!"
+ "MENU_SIMPLE_SUBSCRIPTION": "⚡ Quick purchase"
}
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index 53ef53cd..ca6da8ca 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -1375,85 +1375,5 @@
"SIMPLE_SUBSCRIPTION_SERVER_ANY": "Любой доступный",
"SIMPLE_SUBSCRIPTION_SERVER_SELECTED": "Выбранный",
"SIMPLE_SUBSCRIPTION_SERVER_ASSIGNED": "Назначен автоматически",
- "MENU_SIMPLE_SUBSCRIPTION": "⚡ Простая покупка",
- "ADMIN_COMMUNICATIONS_POLLS": "📋 Опросы",
- "ADMIN_POLLS_TITLE": "📋 Опросы",
- "ADMIN_POLLS_DESCRIPTION": "Создавайте опросы и отправляйте их пользователям по выбранным категориям.",
- "ADMIN_POLLS_REWARD_ENABLED": "есть награда",
- "ADMIN_POLLS_REWARD_DISABLED": "без награды",
- "ADMIN_POLLS_CREATE": "➕ Создать опрос",
- "ADMIN_POLLS_ENTER_TITLE": "🆕 Создание опроса\n\nВведите заголовок опроса.",
- "ADMIN_POLLS_ENTER_TITLE_RETRY": "❗️ Укажите непустой заголовок.",
- "ADMIN_POLLS_ENTER_DESCRIPTION": "✍️ Введите описание опроса. HTML-разметка поддерживается.",
- "ADMIN_POLLS_ENTER_DESCRIPTION_RETRY": "❗️ Описание не может быть пустым.",
- "ADMIN_POLLS_ENTER_QUESTION": "❓ Отправьте текст первого вопроса опроса.",
- "ADMIN_POLLS_ENTER_QUESTION_RETRY": "❗️ Текст вопроса не может быть пустым. Отправьте вопрос ещё раз.",
- "ADMIN_POLLS_ENTER_OPTIONS": "🔢 Отправьте варианты ответов, каждый с новой строки (минимум 2, максимум 10).",
- "ADMIN_POLLS_NEED_MORE_OPTIONS": "❗️ Укажите минимум два варианта ответа.",
- "ADMIN_POLLS_TOO_MANY_OPTIONS": "❗️ Максимум 10 вариантов ответа. Отправьте список ещё раз.",
- "ADMIN_POLLS_QUESTION_NOT_FOUND": "❌ Не удалось найти текст вопроса. Начните заново, выбрав создание вопроса.",
- "ADMIN_POLLS_ADD_QUESTION": "➕ Добавить ещё вопрос",
- "ADMIN_POLLS_CONFIGURE_REWARD": "🎁 Настроить награду",
- "ADMIN_POLLS_CANCEL": "❌ Отмена",
- "ADMIN_POLLS_QUESTION_ADDED": "✅ Вопрос добавлен. Выберите действие:",
- "ADMIN_POLLS_ENTER_QUESTION_NEXT": "❓ Отправьте текст следующего вопроса.",
- "ADMIN_POLLS_NO_QUESTIONS": "— вопросы не добавлены —",
- "ADMIN_POLLS_REWARD_TITLE": "🎁 Награда за участие",
- "ADMIN_POLLS_REWARD_STATUS": "Статус: {status}",
- "ADMIN_POLLS_REWARD_AMOUNT": "Сумма: {amount}",
- "ADMIN_POLLS_REWARD_QUESTIONS": "Всего вопросов: {count}",
- "ADMIN_POLLS_REWARD_DISABLE": "🚫 Отключить награду",
- "ADMIN_POLLS_REWARD_ENABLE": "🔔 Включить награду",
- "ADMIN_POLLS_REWARD_SET_AMOUNT": "💰 Изменить сумму",
- "ADMIN_POLLS_SAVE": "✅ Сохранить опрос",
- "ADMIN_POLLS_ADD_MORE": "➕ Добавить ещё вопрос",
- "ADMIN_POLLS_NEED_QUESTION_FIRST": "Добавьте хотя бы один вопрос перед настройкой награды.",
- "ADMIN_POLLS_REWARD_AMOUNT_PROMPT": "💰 Введите сумму награды в рублях (можно с копейками).",
- "ADMIN_POLLS_REWARD_AMOUNT_INVALID": "❗️ Не удалось распознать число. Введите сумму в формате 10 или 12.5",
- "ADMIN_POLLS_REWARD_AMOUNT_NEGATIVE": "❗️ Сумма не может быть отрицательной.",
- "ADMIN_POLLS_MISSING_DATA": "Заполните заголовок и описание перед сохранением.",
- "ADMIN_POLLS_REWARD_ON": "Включена",
- "ADMIN_POLLS_REWARD_OFF": "Отключена",
- "ADMIN_POLLS_REWARD_SUMMARY": "🎁 Награда: {amount}",
- "ADMIN_POLLS_REWARD_SUMMARY_NONE": "🎁 Награда: не выдается",
- "ADMIN_POLLS_CREATED": "✅ Опрос сохранён!",
- "ADMIN_POLLS_QUESTIONS_COUNT": "Вопросов: {count}",
- "ADMIN_POLLS_OPEN": "📋 К опросу",
- "ADMIN_POLLS_SAVE_ERROR": "❌ Не удалось сохранить опрос. Попробуйте ещё раз позже.",
- "ADMIN_POLLS_NOT_FOUND": "Опрос не найден или был удалён.",
- "ADMIN_POLLS_DESCRIPTION_LABEL": "Описание:",
- "ADMIN_POLLS_STATS_SENT": "Отправлено сообщений: {count}",
- "ADMIN_POLLS_STATS_COMPLETED": "Завершили опрос: {count}",
- "ADMIN_POLLS_QUESTIONS_LIST": "Вопросы:",
- "ADMIN_POLLS_SEND": "🚀 Отправить",
- "ADMIN_POLLS_STATS_BUTTON": "📊 Статистика",
- "ADMIN_POLLS_DELETE": "🗑️ Удалить",
- "ADMIN_POLLS_SELECT_TARGET": "🎯 Выберите категорию пользователей для отправки опроса.",
- "ADMIN_POLLS_CONFIRM_SEND": "✅ Отправить",
- "ADMIN_POLLS_CONFIRMATION_TITLE": "📨 Подтверждение отправки",
- "ADMIN_POLLS_CONFIRMATION_BODY": "Категория: {category}\nПользователей: {count}",
- "ADMIN_POLLS_CONFIRMATION_HINT": "После отправки пользователи получат приглашение пройти опрос.",
- "ADMIN_POLLS_NO_USERS": "Подходящих пользователей не найдено для выбранной категории.",
- "ADMIN_POLLS_SENDING": "📨 Отправляем опрос...",
- "ADMIN_POLLS_SENT": "✅ Отправка завершена!",
- "ADMIN_POLLS_SENT_SUCCESS": "Успешно отправлено: {count}",
- "ADMIN_POLLS_SENT_FAILED": "Ошибок доставки: {count}",
- "ADMIN_POLLS_SENT_TOTAL": "Всего пользователей: {count}",
- "ADMIN_POLLS_STATS_TITLE": "📊 Статистика опроса",
- "ADMIN_POLLS_STATS_RESPONDED": "Ответов получено: {count}",
- "ADMIN_POLLS_STATS_COMPLETED_LABEL": "Прошли до конца: {count}",
- "ADMIN_POLLS_STATS_REWARD_TOTAL": "Выдано наград: {amount}",
- "ADMIN_POLLS_STATS_OPTION": "• {text} — {count} ({percent}%)",
- "ADMIN_POLLS_STATS_NO_DATA": "Ответов пока нет.",
- "ADMIN_POLLS_DELETE_CONFIRM": "🗑️ Удалить",
- "ADMIN_POLLS_DELETE_PROMPT": "❓ Удалить опрос? Это действие нельзя отменить.",
- "ADMIN_POLLS_DELETED": "🗑️ Опрос удалён.",
- "ADMIN_POLLS_BACK_TO_LIST": "⬅️ К списку опросов",
- "POLL_NOT_AVAILABLE": "Опрос недоступен.",
- "POLL_ALREADY_PASSED": "Вы уже участвовали в этом опросе.",
- "POLL_NO_QUESTIONS": "Вопросы опроса не найдены.",
- "POLL_OPTION_ALREADY_CHOSEN": "Ответ уже выбран.",
- "POLL_REWARD_DESCRIPTION": "Награда за участие в опросе '{title}'",
- "POLL_REWARD_RECEIVED": "\n\n🎁 На баланс зачислено: {amount}",
- "POLL_COMPLETED": "🙏 Спасибо за участие в опросе!"
+ "MENU_SIMPLE_SUBSCRIPTION": "⚡ Простая покупка"
}
diff --git a/app/states.py b/app/states.py
index b88ce142..549e314f 100644
--- a/app/states.py
+++ b/app/states.py
@@ -70,12 +70,6 @@ class AdminStates(StatesGroup):
waiting_for_broadcast_media = State()
confirming_broadcast = State()
- creating_poll_title = State()
- creating_poll_description = State()
- creating_poll_question_text = State()
- creating_poll_question_options = State()
- creating_poll_reward_amount = State()
-
creating_promo_group_name = State()
creating_promo_group_traffic_discount = State()
creating_promo_group_server_discount = State()
diff --git a/migrations/alembic/versions/a3f94c8b91dd_add_polls_tables.py b/migrations/alembic/versions/a3f94c8b91dd_add_polls_tables.py
deleted file mode 100644
index 31da4428..00000000
--- a/migrations/alembic/versions/a3f94c8b91dd_add_polls_tables.py
+++ /dev/null
@@ -1,238 +0,0 @@
-"""add polls tables"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.engine.reflection import Inspector
-
-revision: str = "a3f94c8b91dd"
-down_revision: Union[str, None] = "8fd1e338eb45"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-POLL_TABLE = "polls"
-POLL_QUESTIONS_TABLE = "poll_questions"
-POLL_OPTIONS_TABLE = "poll_options"
-POLL_RUNS_TABLE = "poll_runs"
-POLL_RESPONSES_TABLE = "poll_responses"
-POLL_ANSWERS_TABLE = "poll_answers"
-
-
-def _table_exists(inspector: Inspector, table_name: str) -> bool:
- return table_name in inspector.get_table_names()
-
-
-def upgrade() -> None:
- bind = op.get_bind()
- inspector = sa.inspect(bind)
-
- if not _table_exists(inspector, POLL_TABLE):
- op.create_table(
- POLL_TABLE,
- sa.Column("id", sa.Integer(), primary_key=True),
- sa.Column("title", sa.String(length=255), nullable=False),
- sa.Column("description", sa.Text(), nullable=False),
- sa.Column(
- "reward_enabled",
- sa.Boolean(),
- nullable=False,
- server_default=sa.text("false"),
- ),
- sa.Column(
- "reward_amount_kopeks",
- sa.Integer(),
- nullable=False,
- server_default="0",
- ),
- sa.Column("created_by", sa.Integer(), nullable=True),
- sa.Column(
- "created_at",
- sa.DateTime(),
- nullable=False,
- server_default=sa.func.now(),
- ),
- sa.Column(
- "updated_at",
- sa.DateTime(),
- nullable=False,
- server_default=sa.func.now(),
- ),
- sa.ForeignKeyConstraint(["created_by"], ["users.id"], ondelete="SET NULL"),
- )
- inspector = sa.inspect(bind)
-
- if not _table_exists(inspector, POLL_QUESTIONS_TABLE):
- op.create_table(
- POLL_QUESTIONS_TABLE,
- sa.Column("id", sa.Integer(), primary_key=True),
- sa.Column("poll_id", sa.Integer(), nullable=False),
- sa.Column(
- "order",
- sa.Integer(),
- nullable=False,
- server_default="0",
- ),
- sa.Column("text", sa.Text(), nullable=False),
- sa.ForeignKeyConstraint(["poll_id"], [f"{POLL_TABLE}.id"], ondelete="CASCADE"),
- )
- inspector = sa.inspect(bind)
-
- if not _table_exists(inspector, POLL_OPTIONS_TABLE):
- op.create_table(
- POLL_OPTIONS_TABLE,
- sa.Column("id", sa.Integer(), primary_key=True),
- sa.Column("question_id", sa.Integer(), nullable=False),
- sa.Column(
- "order",
- sa.Integer(),
- nullable=False,
- server_default="0",
- ),
- sa.Column("text", sa.String(length=255), nullable=False),
- sa.ForeignKeyConstraint(
- ["question_id"],
- [f"{POLL_QUESTIONS_TABLE}.id"],
- ondelete="CASCADE",
- ),
- )
- inspector = sa.inspect(bind)
-
- if not _table_exists(inspector, POLL_RUNS_TABLE):
- op.create_table(
- POLL_RUNS_TABLE,
- sa.Column("id", sa.Integer(), primary_key=True),
- sa.Column("poll_id", sa.Integer(), nullable=False),
- sa.Column("target_type", sa.String(length=100), nullable=False),
- sa.Column(
- "status",
- sa.String(length=50),
- nullable=False,
- server_default="scheduled",
- ),
- sa.Column(
- "total_count",
- sa.Integer(),
- nullable=False,
- server_default="0",
- ),
- sa.Column(
- "sent_count",
- sa.Integer(),
- nullable=False,
- server_default="0",
- ),
- sa.Column(
- "failed_count",
- sa.Integer(),
- nullable=False,
- server_default="0",
- ),
- sa.Column(
- "completed_count",
- sa.Integer(),
- nullable=False,
- server_default="0",
- ),
- sa.Column("created_by", sa.Integer(), nullable=True),
- sa.Column(
- "created_at",
- sa.DateTime(timezone=True),
- nullable=False,
- server_default=sa.func.now(),
- ),
- sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
- sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
- sa.ForeignKeyConstraint(["poll_id"], [f"{POLL_TABLE}.id"], ondelete="CASCADE"),
- sa.ForeignKeyConstraint(["created_by"], ["users.id"], ondelete="SET NULL"),
- )
- inspector = sa.inspect(bind)
-
- if not _table_exists(inspector, POLL_RESPONSES_TABLE):
- op.create_table(
- POLL_RESPONSES_TABLE,
- sa.Column("id", sa.Integer(), primary_key=True),
- sa.Column("poll_id", sa.Integer(), nullable=False),
- sa.Column("run_id", sa.Integer(), nullable=True),
- sa.Column("user_id", sa.Integer(), nullable=False),
- sa.Column("current_question_id", sa.Integer(), nullable=True),
- sa.Column("message_id", sa.Integer(), nullable=True),
- sa.Column("chat_id", sa.BigInteger(), nullable=True),
- sa.Column(
- "is_completed",
- sa.Boolean(),
- nullable=False,
- server_default=sa.text("false"),
- ),
- sa.Column(
- "reward_given",
- sa.Boolean(),
- nullable=False,
- server_default=sa.text("false"),
- ),
- sa.Column(
- "reward_amount_kopeks",
- sa.Integer(),
- nullable=False,
- server_default="0",
- ),
- sa.Column(
- "created_at",
- sa.DateTime(timezone=True),
- nullable=False,
- server_default=sa.func.now(),
- ),
- sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
- sa.ForeignKeyConstraint(["poll_id"], [f"{POLL_TABLE}.id"], ondelete="CASCADE"),
- sa.ForeignKeyConstraint(["run_id"], [f"{POLL_RUNS_TABLE}.id"], ondelete="SET NULL"),
- sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
- sa.ForeignKeyConstraint(["current_question_id"], [f"{POLL_QUESTIONS_TABLE}.id"], ondelete="SET NULL"),
- sa.UniqueConstraint("poll_id", "user_id", name="uq_poll_user"),
- )
- inspector = sa.inspect(bind)
-
- if not _table_exists(inspector, POLL_ANSWERS_TABLE):
- op.create_table(
- POLL_ANSWERS_TABLE,
- sa.Column("id", sa.Integer(), primary_key=True),
- sa.Column("response_id", sa.Integer(), nullable=False),
- sa.Column("question_id", sa.Integer(), nullable=False),
- sa.Column("option_id", sa.Integer(), nullable=False),
- sa.Column(
- "created_at",
- sa.DateTime(timezone=True),
- nullable=False,
- server_default=sa.func.now(),
- ),
- sa.ForeignKeyConstraint(["response_id"], [f"{POLL_RESPONSES_TABLE}.id"], ondelete="CASCADE"),
- sa.ForeignKeyConstraint(["question_id"], [f"{POLL_QUESTIONS_TABLE}.id"], ondelete="CASCADE"),
- sa.ForeignKeyConstraint(["option_id"], [f"{POLL_OPTIONS_TABLE}.id"], ondelete="CASCADE"),
- )
-
-
-def downgrade() -> None:
- bind = op.get_bind()
- inspector = sa.inspect(bind)
-
- if _table_exists(inspector, POLL_ANSWERS_TABLE):
- op.drop_table(POLL_ANSWERS_TABLE)
- inspector = sa.inspect(bind)
-
- if _table_exists(inspector, POLL_RESPONSES_TABLE):
- op.drop_table(POLL_RESPONSES_TABLE)
- inspector = sa.inspect(bind)
-
- if _table_exists(inspector, POLL_RUNS_TABLE):
- op.drop_table(POLL_RUNS_TABLE)
- inspector = sa.inspect(bind)
-
- if _table_exists(inspector, POLL_OPTIONS_TABLE):
- op.drop_table(POLL_OPTIONS_TABLE)
- inspector = sa.inspect(bind)
-
- if _table_exists(inspector, POLL_QUESTIONS_TABLE):
- op.drop_table(POLL_QUESTIONS_TABLE)
- inspector = sa.inspect(bind)
-
- if _table_exists(inspector, POLL_TABLE):
- op.drop_table(POLL_TABLE)