Add poll management and delivery system
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import logging
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
from sqlalchemy import and_, delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.database.models import (
|
||||
Poll,
|
||||
PollAnswer,
|
||||
PollOption,
|
||||
PollQuestion,
|
||||
PollResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_poll(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
title: str,
|
||||
description: str | None,
|
||||
reward_enabled: bool,
|
||||
reward_amount_kopeks: int,
|
||||
created_by: int | None,
|
||||
questions: Sequence[dict[str, Iterable[str]]],
|
||||
) -> Poll:
|
||||
poll = Poll(
|
||||
title=title,
|
||||
description=description,
|
||||
reward_enabled=reward_enabled,
|
||||
reward_amount_kopeks=reward_amount_kopeks if reward_enabled else 0,
|
||||
created_by=created_by,
|
||||
)
|
||||
db.add(poll)
|
||||
await db.flush()
|
||||
|
||||
for order, question_data in enumerate(questions, start=1):
|
||||
question_text = question_data.get("text", "").strip()
|
||||
if not question_text:
|
||||
continue
|
||||
|
||||
question = PollQuestion(
|
||||
poll_id=poll.id,
|
||||
text=question_text,
|
||||
order=order,
|
||||
)
|
||||
db.add(question)
|
||||
await db.flush()
|
||||
|
||||
for option_order, option_text in enumerate(question_data.get("options", []), start=1):
|
||||
option_text = option_text.strip()
|
||||
if not option_text:
|
||||
continue
|
||||
option = PollOption(
|
||||
question_id=question.id,
|
||||
text=option_text,
|
||||
order=option_order,
|
||||
)
|
||||
db.add(option)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(
|
||||
poll,
|
||||
attribute_names=["questions"],
|
||||
)
|
||||
return poll
|
||||
|
||||
|
||||
async def list_polls(db: AsyncSession) -> list[Poll]:
|
||||
result = await db.execute(
|
||||
select(Poll)
|
||||
.options(
|
||||
selectinload(Poll.questions).options(selectinload(PollQuestion.options))
|
||||
)
|
||||
.order_by(Poll.created_at.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_poll_by_id(db: AsyncSession, poll_id: int) -> Poll | None:
|
||||
result = await db.execute(
|
||||
select(Poll)
|
||||
.options(
|
||||
selectinload(Poll.questions).options(selectinload(PollQuestion.options)),
|
||||
selectinload(Poll.responses),
|
||||
)
|
||||
.where(Poll.id == poll_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def delete_poll(db: AsyncSession, poll_id: int) -> bool:
|
||||
poll = await db.get(Poll, poll_id)
|
||||
if not poll:
|
||||
return False
|
||||
|
||||
await db.delete(poll)
|
||||
await db.commit()
|
||||
logger.info("🗑️ Удалён опрос %s", poll_id)
|
||||
return True
|
||||
|
||||
|
||||
async def create_poll_response(
|
||||
db: AsyncSession,
|
||||
poll_id: int,
|
||||
user_id: int,
|
||||
) -> PollResponse:
|
||||
result = await db.execute(
|
||||
select(PollResponse)
|
||||
.where(
|
||||
and_(
|
||||
PollResponse.poll_id == poll_id,
|
||||
PollResponse.user_id == user_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
response = result.scalar_one_or_none()
|
||||
if response:
|
||||
return response
|
||||
|
||||
response = PollResponse(
|
||||
poll_id=poll_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
db.add(response)
|
||||
await db.commit()
|
||||
await db.refresh(response)
|
||||
return response
|
||||
|
||||
|
||||
async def get_poll_response_by_id(
|
||||
db: AsyncSession,
|
||||
response_id: int,
|
||||
) -> PollResponse | None:
|
||||
result = await db.execute(
|
||||
select(PollResponse)
|
||||
.options(
|
||||
selectinload(PollResponse.poll)
|
||||
.options(selectinload(Poll.questions).options(selectinload(PollQuestion.options))),
|
||||
selectinload(PollResponse.answers),
|
||||
selectinload(PollResponse.user),
|
||||
)
|
||||
.where(PollResponse.id == response_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def record_poll_answer(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
response_id: int,
|
||||
question_id: int,
|
||||
option_id: int,
|
||||
) -> PollAnswer:
|
||||
result = await db.execute(
|
||||
select(PollAnswer)
|
||||
.where(
|
||||
and_(
|
||||
PollAnswer.response_id == response_id,
|
||||
PollAnswer.question_id == question_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
answer = result.scalar_one_or_none()
|
||||
if answer:
|
||||
answer.option_id = option_id
|
||||
await db.commit()
|
||||
await db.refresh(answer)
|
||||
return answer
|
||||
|
||||
answer = PollAnswer(
|
||||
response_id=response_id,
|
||||
question_id=question_id,
|
||||
option_id=option_id,
|
||||
)
|
||||
db.add(answer)
|
||||
await db.commit()
|
||||
await db.refresh(answer)
|
||||
return answer
|
||||
|
||||
|
||||
async def reset_poll_answers(db: AsyncSession, response_id: int) -> None:
|
||||
await db.execute(
|
||||
delete(PollAnswer).where(PollAnswer.response_id == response_id)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_poll_statistics(db: AsyncSession, poll_id: int) -> dict:
|
||||
totals_result = await db.execute(
|
||||
select(
|
||||
func.count(PollResponse.id),
|
||||
func.count(PollResponse.completed_at),
|
||||
func.coalesce(func.sum(PollResponse.reward_amount_kopeks), 0),
|
||||
).where(PollResponse.poll_id == poll_id)
|
||||
)
|
||||
total_responses, completed_responses, reward_sum = totals_result.one()
|
||||
|
||||
option_counts_result = await db.execute(
|
||||
select(
|
||||
PollQuestion.id,
|
||||
PollQuestion.text,
|
||||
PollQuestion.order,
|
||||
PollOption.id,
|
||||
PollOption.text,
|
||||
PollOption.order,
|
||||
func.count(PollAnswer.id),
|
||||
)
|
||||
.join(PollOption, PollOption.question_id == PollQuestion.id)
|
||||
.outerjoin(
|
||||
PollAnswer,
|
||||
and_(
|
||||
PollAnswer.question_id == PollQuestion.id,
|
||||
PollAnswer.option_id == PollOption.id,
|
||||
),
|
||||
)
|
||||
.where(PollQuestion.poll_id == poll_id)
|
||||
.group_by(
|
||||
PollQuestion.id,
|
||||
PollQuestion.text,
|
||||
PollQuestion.order,
|
||||
PollOption.id,
|
||||
PollOption.text,
|
||||
PollOption.order,
|
||||
)
|
||||
.order_by(PollQuestion.order.asc(), PollOption.order.asc())
|
||||
)
|
||||
|
||||
questions_map: dict[int, dict] = {}
|
||||
for (
|
||||
question_id,
|
||||
question_text,
|
||||
question_order,
|
||||
option_id,
|
||||
option_text,
|
||||
option_order,
|
||||
answer_count,
|
||||
) in option_counts_result:
|
||||
question_entry = questions_map.setdefault(
|
||||
question_id,
|
||||
{
|
||||
"id": question_id,
|
||||
"text": question_text,
|
||||
"order": question_order,
|
||||
"options": [],
|
||||
},
|
||||
)
|
||||
question_entry["options"].append(
|
||||
{
|
||||
"id": option_id,
|
||||
"text": option_text,
|
||||
"count": answer_count,
|
||||
}
|
||||
)
|
||||
|
||||
questions = sorted(questions_map.values(), key=lambda item: item["order"])
|
||||
|
||||
return {
|
||||
"total_responses": total_responses,
|
||||
"completed_responses": completed_responses,
|
||||
"reward_sum_kopeks": reward_sum,
|
||||
"questions": questions,
|
||||
}
|
||||
@@ -219,7 +219,8 @@ async def add_user_balance(
|
||||
amount_kopeks: int,
|
||||
description: str = "Пополнение баланса",
|
||||
create_transaction: bool = True,
|
||||
bot = None
|
||||
transaction_type: TransactionType = TransactionType.DEPOSIT,
|
||||
bot = None
|
||||
) -> bool:
|
||||
try:
|
||||
old_balance = user.balance_kopeks
|
||||
@@ -228,12 +229,11 @@ async def add_user_balance(
|
||||
|
||||
if create_transaction:
|
||||
from app.database.crud.transaction import create_transaction as create_trans
|
||||
from app.database.models import TransactionType
|
||||
|
||||
|
||||
await create_trans(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
type=TransactionType.DEPOSIT,
|
||||
type=transaction_type,
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=description
|
||||
)
|
||||
@@ -253,9 +253,10 @@ async def add_user_balance(
|
||||
|
||||
async def add_user_balance_by_id(
|
||||
db: AsyncSession,
|
||||
telegram_id: int,
|
||||
telegram_id: int,
|
||||
amount_kopeks: int,
|
||||
description: str = "Пополнение баланса"
|
||||
description: str = "Пополнение баланса",
|
||||
transaction_type: TransactionType = TransactionType.DEPOSIT,
|
||||
) -> bool:
|
||||
try:
|
||||
user = await get_user_by_telegram_id(db, telegram_id)
|
||||
@@ -263,7 +264,13 @@ async def add_user_balance_by_id(
|
||||
logger.error(f"Пользователь с telegram_id {telegram_id} не найден")
|
||||
return False
|
||||
|
||||
return await add_user_balance(db, user, amount_kopeks, description)
|
||||
return await add_user_balance(
|
||||
db,
|
||||
user,
|
||||
amount_kopeks,
|
||||
description,
|
||||
transaction_type=transaction_type,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка пополнения баланса пользователя {telegram_id}: {e}")
|
||||
|
||||
Reference in New Issue
Block a user