mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Revert "Fix aiogram Bot usage in poll handlers"
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"📋 <b>{html.escape(poll.title)}</b>\n\n"
|
||||
body = ""
|
||||
if include_description:
|
||||
body += f"{poll.description}\n\n"
|
||||
body += (
|
||||
f"❓ <b>{question_index}/{total_questions}</b>\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_"))
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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": "📋 <b>Polls</b>",
|
||||
"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": "🆕 <b>Create a poll</b>\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": "🎁 <b>Reward settings</b>",
|
||||
"ADMIN_POLLS_REWARD_STATUS": "Status: <b>{status}</b>",
|
||||
"ADMIN_POLLS_REWARD_AMOUNT": "Amount: <b>{amount}</b>",
|
||||
"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: <b>{count}</b>",
|
||||
"ADMIN_POLLS_STATS_COMPLETED": "Finished the poll: <b>{count}</b>",
|
||||
"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: <b>{category}</b>\nUsers: <b>{count}</b>",
|
||||
"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: <b>{count}</b>",
|
||||
"ADMIN_POLLS_SENT_FAILED": "Failed deliveries: <b>{count}</b>",
|
||||
"ADMIN_POLLS_SENT_TOTAL": "Total recipients: <b>{count}</b>",
|
||||
"ADMIN_POLLS_STATS_TITLE": "📊 Poll statistics",
|
||||
"ADMIN_POLLS_STATS_RESPONDED": "Responses received: <b>{count}</b>",
|
||||
"ADMIN_POLLS_STATS_COMPLETED_LABEL": "Completed: <b>{count}</b>",
|
||||
"ADMIN_POLLS_STATS_REWARD_TOTAL": "Rewards issued: <b>{amount}</b>",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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": "📋 <b>Опросы</b>",
|
||||
"ADMIN_POLLS_DESCRIPTION": "Создавайте опросы и отправляйте их пользователям по выбранным категориям.",
|
||||
"ADMIN_POLLS_REWARD_ENABLED": "есть награда",
|
||||
"ADMIN_POLLS_REWARD_DISABLED": "без награды",
|
||||
"ADMIN_POLLS_CREATE": "➕ Создать опрос",
|
||||
"ADMIN_POLLS_ENTER_TITLE": "🆕 <b>Создание опроса</b>\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": "🎁 <b>Награда за участие</b>",
|
||||
"ADMIN_POLLS_REWARD_STATUS": "Статус: <b>{status}</b>",
|
||||
"ADMIN_POLLS_REWARD_AMOUNT": "Сумма: <b>{amount}</b>",
|
||||
"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": "Отправлено сообщений: <b>{count}</b>",
|
||||
"ADMIN_POLLS_STATS_COMPLETED": "Завершили опрос: <b>{count}</b>",
|
||||
"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": "Категория: <b>{category}</b>\nПользователей: <b>{count}</b>",
|
||||
"ADMIN_POLLS_CONFIRMATION_HINT": "После отправки пользователи получат приглашение пройти опрос.",
|
||||
"ADMIN_POLLS_NO_USERS": "Подходящих пользователей не найдено для выбранной категории.",
|
||||
"ADMIN_POLLS_SENDING": "📨 Отправляем опрос...",
|
||||
"ADMIN_POLLS_SENT": "✅ Отправка завершена!",
|
||||
"ADMIN_POLLS_SENT_SUCCESS": "Успешно отправлено: <b>{count}</b>",
|
||||
"ADMIN_POLLS_SENT_FAILED": "Ошибок доставки: <b>{count}</b>",
|
||||
"ADMIN_POLLS_SENT_TOTAL": "Всего пользователей: <b>{count}</b>",
|
||||
"ADMIN_POLLS_STATS_TITLE": "📊 Статистика опроса",
|
||||
"ADMIN_POLLS_STATS_RESPONDED": "Ответов получено: <b>{count}</b>",
|
||||
"ADMIN_POLLS_STATS_COMPLETED_LABEL": "Прошли до конца: <b>{count}</b>",
|
||||
"ADMIN_POLLS_STATS_REWARD_TOTAL": "Выдано наград: <b>{amount}</b>",
|
||||
"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": "⚡ Простая покупка"
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user