Compare commits
3 Commits
515f5924f3
...
878a1e1a49
| Author | SHA1 | Date | |
|---|---|---|---|
| 878a1e1a49 | |||
| becf4f5c99 | |||
| 842710fe5c |
@@ -3,7 +3,7 @@ from django.urls import path
|
|||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import BotConfig
|
from .models import BotConfig
|
||||||
from .models import BotConfig, WelcomeMessage
|
from .models import BotConfig, WelcomeMessage, BotMessage, BotEventMessageConfig
|
||||||
from .tasks import restart_bot_container
|
from .tasks import restart_bot_container
|
||||||
|
|
||||||
@admin.register(BotConfig)
|
@admin.register(BotConfig)
|
||||||
@@ -35,4 +35,12 @@ class BotConfigAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(WelcomeMessage)
|
@admin.register(WelcomeMessage)
|
||||||
class WWelcomeMessageAdmin(admin.ModelAdmin):
|
class WWelcomeMessageAdmin(admin.ModelAdmin):
|
||||||
list_display = ("bot", "welcome_message", "welcome_image", "admin_contact", "channel_link", "group_link", "custom_link1_name", "custom_link1_url")
|
list_display = ("bot", "welcome_message", "welcome_image", "admin_contact", "channel_link", "group_link", "custom_link1_name", "custom_link1_url")
|
||||||
|
|
||||||
|
@admin.register(BotMessage)
|
||||||
|
class BotMessageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "text", "image", "buttons_json")
|
||||||
|
|
||||||
|
@admin.register(BotEventMessageConfig)
|
||||||
|
class BotEventMessageConfigAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("event", "message", "enabled")
|
||||||
|
list_filter = ("event", "enabled")
|
||||||
@@ -93,7 +93,7 @@ async def handle_client_card(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
|
|
||||||
BINDING_PENDING.add(telegram_chat_id)
|
BINDING_PENDING.add(telegram_chat_id)
|
||||||
await update.message.reply_text("Введите номер вашей клубной карты (КК):")
|
await update.message.reply_text("Введите номер вашей клубной карты (КК):")
|
||||||
|
await send_event_message("binding_started", update, context)
|
||||||
|
|
||||||
async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def process_binding_input(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if not update.message or not update.message.text:
|
if not update.message or not update.message.text:
|
||||||
@@ -124,7 +124,7 @@ async def show_all_invoices_callback(update: Update, context: ContextTypes.DEFAU
|
|||||||
message_text = "💳 *Все счета:*\n"
|
message_text = "💳 *Все счета:*\n"
|
||||||
if invoices:
|
if invoices:
|
||||||
for inv in invoices:
|
for inv in invoices:
|
||||||
message_text += f" • ID {inv.id} (*{inv.sum}*)\n"
|
message_text += f" • ID {inv.ext_id} (*{inv.sum}*)\n"
|
||||||
else:
|
else:
|
||||||
message_text += " _Нет счетов_\n"
|
message_text += " _Нет счетов_\n"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-08-03 05:29
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bot', '0007_botconfig_active_welcome_botconfig_is_active_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BotMessage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Название сообщения')),
|
||||||
|
('text', models.TextField(blank=True, verbose_name='Текст')),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='bot_messages/', verbose_name='Картинка')),
|
||||||
|
('buttons_json', models.JSONField(blank=True, null=True, verbose_name='Кнопки (JSON-формат)')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BotEventMessageConfig',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('event', models.CharField(choices=[('welcome', 'Приветствие'), ('draw_started', 'Розыгрыш начат'), ('draw_finished', 'Розыгрыш завершен'), ('winner_announced', 'Объявление победителя')], max_length=50, unique=True, verbose_name='Событие')),
|
||||||
|
('enabled', models.BooleanField(default=True, verbose_name='Активно')),
|
||||||
|
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_configs', to='bot.botmessage')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-08-03 05:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bot', '0008_botmessage_boteventmessageconfig'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='boteventmessageconfig',
|
||||||
|
options={'verbose_name': 'Событие сообщения', 'verbose_name_plural': 'События сообщений'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='botmessage',
|
||||||
|
options={'verbose_name': 'Сообщение', 'verbose_name_plural': 'Сообщения'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='boteventmessageconfig',
|
||||||
|
name='event',
|
||||||
|
field=models.CharField(choices=[('welcome', 'Приветствие'), ('draw_started', 'Розыгрыш начат'), ('draw_finished', 'Розыгрыш завершен'), ('winner_announced', 'Объявление победителя'), ('guest_binding', 'Подтверждение гостя')], max_length=50, unique=True, verbose_name='Событие'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-08-03 11:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bot', '0009_alter_boteventmessageconfig_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='welcomemessage',
|
||||||
|
name='welcome_image',
|
||||||
|
field=models.ImageField(blank=True, help_text='Загрузите изображение для приветствия', null=True, upload_to='welcome_messages/', verbose_name='Приветственное изображение'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BotConfig(models.Model):
|
class BotConfig(models.Model):
|
||||||
bot_token = models.CharField(max_length=255, help_text="Токен для подключения к Telegram API")
|
bot_token = models.CharField(max_length=255, help_text="Токен для подключения к Telegram API")
|
||||||
channel_id = models.CharField(max_length=100, help_text="ID канала/чата, куда бот будет отправлять сообщения")
|
channel_id = models.CharField(max_length=100, help_text="ID канала/чата, куда бот будет отправлять сообщения")
|
||||||
@@ -44,7 +45,7 @@ class WelcomeMessage(models.Model):
|
|||||||
help_text="Текст, который будет отправлен при запуске команды /start"
|
help_text="Текст, который будет отправлен при запуске команды /start"
|
||||||
)
|
)
|
||||||
welcome_image = models.ImageField(
|
welcome_image = models.ImageField(
|
||||||
upload_to='static/upload_image/',
|
upload_to='welcome_messages/',
|
||||||
verbose_name="Приветственное изображение",
|
verbose_name="Приветственное изображение",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
@@ -98,3 +99,38 @@ class WelcomeMessage(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Приветствие для {self.bot}"
|
return f"Приветствие для {self.bot}"
|
||||||
|
|
||||||
|
class BotMessage(models.Model):
|
||||||
|
name = models.CharField("Название сообщения", max_length=100)
|
||||||
|
text = models.TextField("Текст", blank=True)
|
||||||
|
image = models.ImageField("Картинка", upload_to="bot_messages/", blank=True, null=True)
|
||||||
|
buttons_json = models.JSONField("Кнопки (JSON-формат)", blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Сообщение"
|
||||||
|
verbose_name_plural = "Сообщения"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class BotEventMessageConfig(models.Model):
|
||||||
|
EVENT_CHOICES = [
|
||||||
|
('welcome', 'Приветствие'),
|
||||||
|
('draw_started', 'Розыгрыш начат'),
|
||||||
|
('draw_finished', 'Розыгрыш завершен'),
|
||||||
|
('winner_announced', 'Объявление победителя'),
|
||||||
|
('guest_binding', 'Подтверждение гостя'),
|
||||||
|
# и другие
|
||||||
|
]
|
||||||
|
|
||||||
|
event = models.CharField("Событие", max_length=50, choices=EVENT_CHOICES, unique=True)
|
||||||
|
message = models.ForeignKey(BotMessage, on_delete=models.CASCADE, related_name="event_configs")
|
||||||
|
enabled = models.BooleanField("Активно", default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Событие сообщения"
|
||||||
|
verbose_name_plural = "События сообщений"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.get_event_display()} → {self.message.name}"
|
||||||
|
|
||||||
@@ -1,105 +1,131 @@
|
|||||||
# notifications.py
|
|
||||||
import logging
|
import logging
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
|
from bot.utils import send_event_message_async
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
class NotificationService:
|
class NotificationService:
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
"""
|
|
||||||
Инициализация сервиса уведомлений.
|
|
||||||
:param bot: экземпляр telegram.Bot, используемый для отправки сообщений.
|
|
||||||
"""
|
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger("NotificationService")
|
||||||
|
self.logger.setLevel(logging.DEBUG)
|
||||||
|
if not self.logger.handlers:
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
||||||
|
self.logger.addHandler(handler)
|
||||||
|
|
||||||
@sync_to_async
|
@sync_to_async
|
||||||
def _get_all_clients(self):
|
def _get_all_clients(self):
|
||||||
from webapp.models import Client
|
from webapp.models import Client
|
||||||
# Возвращаем только клиентов, у которых telegram_id заполнен корректно
|
clients = list(Client.objects.exclude(telegram_id__in=[None, "", "NULL"]))
|
||||||
return list(Client.objects.exclude(telegram_id__in=[None, "", "NULL"]))
|
self.logger.info(f"Найдено клиентов для уведомления: {len(clients)}")
|
||||||
|
return clients
|
||||||
|
|
||||||
@sync_to_async
|
@sync_to_async
|
||||||
def _prepare_results(self, results):
|
def _prepare_results(self, results):
|
||||||
# Принудительно загружаем связанные объекты через select_related
|
|
||||||
return list(results.select_related('prize', 'participant__invoice'))
|
return list(results.select_related('prize', 'participant__invoice'))
|
||||||
|
|
||||||
async def notify_draw_start(self, lottery):
|
async def notify_draw_start(self, lottery):
|
||||||
"""
|
|
||||||
Уведомляет всех клиентов о запуске розыгрыша.
|
|
||||||
:param lottery: объект розыгрыша (например, модель Lottery)
|
|
||||||
"""
|
|
||||||
message_text = f"🎉 *Розыгрыш '{lottery.name}' начался!* Следите за обновлениями."
|
|
||||||
clients = await self._get_all_clients()
|
clients = await self._get_all_clients()
|
||||||
for client in clients:
|
for client in clients:
|
||||||
if not client.telegram_id:
|
if not client.telegram_id:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
await self.bot.send_message(
|
self.logger.debug(f"Отправка 'draw_started' клиенту {client.telegram_id}")
|
||||||
chat_id=client.telegram_id,
|
await send_event_message_async(
|
||||||
text=message_text,
|
event="draw_started",
|
||||||
parse_mode="Markdown"
|
bot=self.bot,
|
||||||
|
chat_id=int(client.telegram_id),
|
||||||
|
context_vars={"lottery": lottery, "client": client}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Ошибка отправки уведомления о запуске розыгрыша пользователю {client.telegram_id}: {e}")
|
self.logger.error(f"Ошибка отправки draw_started: {e}")
|
||||||
|
|
||||||
async def notify_draw_results(self, lottery, results):
|
# async def notify_draw_results(self, lottery, results):
|
||||||
"""
|
# results_list = await self._prepare_results(results)
|
||||||
Отправляет результаты розыгрыша всем клиентам.
|
# clients = await self._get_all_clients()
|
||||||
:param lottery: объект розыгрыша (Lottery)
|
# for client in clients:
|
||||||
:param results: QuerySet с результатами розыгрыша (например, объекты DrawResult)
|
# if not client.telegram_id:
|
||||||
"""
|
# continue
|
||||||
# Принудительно загружаем связанные объекты
|
# try:
|
||||||
|
# self.logger.debug(f"Отправка 'draw_finished' клиенту {client.telegram_id}")
|
||||||
|
# await send_event_message_async(
|
||||||
|
# event="draw_finished",
|
||||||
|
# bot=self.bot,
|
||||||
|
# chat_id=int(client.telegram_id),
|
||||||
|
# context_vars={"lottery": lottery, "client": client, "results": results_list}
|
||||||
|
# )
|
||||||
|
# except Exception as e:
|
||||||
|
# self.logger.error(f"Ошибка отправки draw_finished: {e}")
|
||||||
|
|
||||||
|
async def notify_draw_results(self, lottery, results, context_vars=None):
|
||||||
results_list = await self._prepare_results(results)
|
results_list = await self._prepare_results(results)
|
||||||
message_text = f"🎉 *Результаты розыгрыша '{lottery.name}':*\n"
|
|
||||||
for result in results_list:
|
|
||||||
status = "✅ Подтвержден" if result.confirmed else "⏳ В ожидании подтверждения"
|
|
||||||
prize = result.prize.reward if result.prize and hasattr(result.prize, "reward") else "неизвестно"
|
|
||||||
account = (str(result.participant.invoice).split('/')[0].strip()
|
|
||||||
if result.participant and hasattr(result.participant, "invoice") else "неизвестно")
|
|
||||||
message_text += f"• Приз: *{prize}*, Счет: _{account}_, Статус: *{status}*\n"
|
|
||||||
clients = await self._get_all_clients()
|
clients = await self._get_all_clients()
|
||||||
|
|
||||||
|
# 🔢 Подготовка суммы и списка победителей
|
||||||
|
total_reward = sum(
|
||||||
|
r.prize.reward for r in results_list if r.prize and r.prize.reward
|
||||||
|
)
|
||||||
|
|
||||||
|
winners_lines = []
|
||||||
|
for r in results_list:
|
||||||
|
try:
|
||||||
|
account_id = r.participant.invoice.ext_id
|
||||||
|
prize_amount = r.prize.reward
|
||||||
|
winners_lines.append(f"• Счёт {account_id} — {prize_amount:,.2f}₩")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
winners_list = "\n".join(winners_lines)
|
||||||
|
|
||||||
for client in clients:
|
for client in clients:
|
||||||
if not client.telegram_id:
|
if not client.telegram_id:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
await self.bot.send_message(
|
self.logger.debug(f"Отправка 'draw_finished' клиенту {client.telegram_id}")
|
||||||
chat_id=client.telegram_id,
|
await send_event_message_async(
|
||||||
text=message_text,
|
event="draw_finished",
|
||||||
parse_mode="Markdown"
|
bot=self.bot,
|
||||||
|
chat_id=int(client.telegram_id),
|
||||||
|
context_vars={
|
||||||
|
**(context_vars or {}),
|
||||||
|
"lottery": lottery,
|
||||||
|
"client": client,
|
||||||
|
"results": results_list,
|
||||||
|
"total_reward": f"{total_reward:,.2f}",
|
||||||
|
"winners_list": winners_list
|
||||||
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Ошибка отправки результатов розыгрыша пользователю {client.telegram_id}: {e}")
|
self.logger.error(f"Ошибка отправки draw_finished: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def notify_prize_status_update(self, client, result):
|
async def notify_prize_status_update(self, client, result):
|
||||||
"""
|
|
||||||
Отправляет конкретному пользователю обновление по статусу подтверждения приза.
|
|
||||||
:param client: объект клиента (Client)
|
|
||||||
:param result: объект результата розыгрыша (DrawResult)
|
|
||||||
"""
|
|
||||||
status = "✅ Подтвержден" if result.confirmed else "⏳ В ожидании подтверждения"
|
|
||||||
prize = result.prize.reward if result.prize and hasattr(result.prize, "reward") else "неизвестно"
|
|
||||||
message_text = f"🎉 *Обновление розыгрыша:*\nВаш приз *{prize}* имеет статус: *{status}*."
|
|
||||||
try:
|
try:
|
||||||
if client.telegram_id:
|
if client.telegram_id:
|
||||||
await self.bot.send_message(
|
self.logger.debug(f"Отправка 'winner_announced' клиенту {client.telegram_id}")
|
||||||
chat_id=client.telegram_id,
|
await send_event_message_async(
|
||||||
text=message_text,
|
event="winner_announced",
|
||||||
parse_mode="Markdown"
|
bot=self.bot,
|
||||||
|
chat_id=int(client.telegram_id),
|
||||||
|
context_vars={"client": client, "result": result, "prize": result.prize}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Ошибка отправки обновления статуса приза пользователю {client.telegram_id}: {e}")
|
self.logger.error(f"Ошибка отправки winner_announced: {e}")
|
||||||
|
|
||||||
async def notify_binding_complete(self, client):
|
async def notify_binding_complete(self, client):
|
||||||
"""
|
|
||||||
Уведомляет пользователя об окончании привязки клубной карты к Telegram ID.
|
|
||||||
:param client: объект клиента (Client)
|
|
||||||
"""
|
|
||||||
message_text = "✅ *Привязка завершена!* Ваша клубная карта успешно привязана к Telegram. Теперь вы можете участвовать в розыгрышах и чате."
|
|
||||||
try:
|
try:
|
||||||
if client.telegram_id:
|
if client.telegram_id:
|
||||||
await self.bot.send_message(
|
self.logger.debug(f"Отправка 'binding_complete' клиенту {client.telegram_id}")
|
||||||
chat_id=client.telegram_id,
|
await send_event_message_async(
|
||||||
text=message_text,
|
event="binding_complete",
|
||||||
parse_mode="Markdown"
|
bot=self.bot,
|
||||||
|
chat_id=int(client.telegram_id),
|
||||||
|
context_vars={"client": client}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Ошибка отправки уведомления о привязке пользователю {client.telegram_id}: {e}")
|
self.logger.error(f"Ошибка отправки binding_complete: {e}")
|
||||||
|
|||||||
@@ -1,39 +1,99 @@
|
|||||||
# bot/utils.py
|
from telegram import Bot, InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
from telegram import Bot
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from telegram import Bot
|
|
||||||
from bot.models import BotConfig
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from bot.models import BotConfig, BotEventMessageConfig
|
||||||
|
import logging
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
from telegram import InputFile
|
||||||
|
import os
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
def create_bot_instance():
|
def create_bot_instance():
|
||||||
"""
|
config = BotConfig.objects.first()
|
||||||
Получает настройки бота из БД и создаёт экземпляр Telegram Bot.
|
|
||||||
"""
|
|
||||||
config = BotConfig.objects.first() # предполагается, что конфигурация одна
|
|
||||||
if not config:
|
if not config:
|
||||||
raise ImproperlyConfigured("Настройки бота (BotConfig) не сконфигурированы в базе данных.")
|
raise ImproperlyConfigured("BotConfig не задан.")
|
||||||
|
|
||||||
# Можно дополнительно использовать config.channel_id и config.bot_name при необходимости.
|
logger.debug("Создан экземпляр бота с токеном.")
|
||||||
bot = Bot(token=config.bot_token)
|
return Bot(token=config.bot_token)
|
||||||
return bot
|
|
||||||
|
|
||||||
|
|
||||||
def notify_user(binding_request, approved=True):
|
def get_event_message_sync(event_key: str, context_vars: dict = None):
|
||||||
bot_token = settings.BOT_CONFIG.bot_token # можно использовать модель BotConfig или настройку из settings
|
logger.debug(f"get_event_message_sync для события: {event_key}")
|
||||||
bot = Bot(token=bot_token)
|
|
||||||
message = "Ваша заявка на привязку успешно подтверждена!" if approved else "Ваша заявка на привязку отклонена."
|
|
||||||
try:
|
try:
|
||||||
bot.send_message(chat_id=binding_request.telegram_chat_id, text=message)
|
config = BotEventMessageConfig.objects.select_related("message").get(event=event_key, enabled=True)
|
||||||
|
msg = config.message
|
||||||
|
context_vars = context_vars or {}
|
||||||
|
|
||||||
|
text = msg.text.format(**context_vars) if msg.text else None
|
||||||
|
image_name = msg.image.name if msg.image else None # только имя файла
|
||||||
|
|
||||||
|
keyboard = None
|
||||||
|
if msg.buttons_json:
|
||||||
|
keyboard = InlineKeyboardMarkup([
|
||||||
|
[InlineKeyboardButton(btn["text"], url=btn["url"])]
|
||||||
|
for btn in msg.buttons_json
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"text": text,
|
||||||
|
"image": image_name,
|
||||||
|
"keyboard": keyboard
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Обработка ошибок отправки сообщения
|
logger.error(f"Ошибка get_event_message_sync для события '{event_key}': {e}")
|
||||||
print(f"Ошибка уведомления: {e}")
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Пример использования в точке входа приложения:
|
def send_event_message_sync(event: str, bot: Bot, chat_id: int, context_vars: dict = None):
|
||||||
if __name__ == '__main__':
|
msg = get_event_message_sync(event, context_vars)
|
||||||
# Предполагается, что Django уже настроен (например, через manage.py shell или management command)
|
if not msg:
|
||||||
bot = create_bot_instance()
|
logger.warning(f"Пропущена отправка: шаблон события '{event}' не найден.")
|
||||||
# Теперь можно использовать объект bot для отправки сообщений, обработки обновлений и т.д.
|
return
|
||||||
print(f"Бот {bot.name} успешно создан!")
|
try:
|
||||||
|
if msg["image"]:
|
||||||
|
bot.send_photo(chat_id=chat_id, photo=msg["image"], caption=msg["text"], reply_markup=msg["keyboard"])
|
||||||
|
elif msg["text"]:
|
||||||
|
bot.send_message(chat_id=chat_id, text=msg["text"], reply_markup=msg["keyboard"])
|
||||||
|
logger.info(f"Сообщение '{event}' успешно отправлено пользователю {chat_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке сообщения '{event}' пользователю {chat_id}: {e}")
|
||||||
|
|
||||||
|
@sync_to_async
|
||||||
|
def get_event_message_async(event_key: str, context_vars: dict = None):
|
||||||
|
return get_event_message_sync(event_key, context_vars)
|
||||||
|
|
||||||
|
async def send_event_message_async(event: str, bot: Bot, chat_id: int, context_vars: dict = None):
|
||||||
|
msg = await get_event_message_async(event, context_vars)
|
||||||
|
if not msg:
|
||||||
|
logger.warning(f"[ASYNC] Пропущена отправка: шаблон события '{event}' не найден.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if msg["image"]:
|
||||||
|
# локальный путь к файлу
|
||||||
|
from django.conf import settings
|
||||||
|
file_path = os.path.join(settings.MEDIA_ROOT, msg["image"])
|
||||||
|
logger.debug(f"[ASYNC] Отправка фото из {file_path}")
|
||||||
|
with open(file_path, "rb") as photo:
|
||||||
|
await bot.send_photo(
|
||||||
|
chat_id=chat_id,
|
||||||
|
photo=InputFile(photo),
|
||||||
|
caption=msg["text"],
|
||||||
|
reply_markup=msg["keyboard"]
|
||||||
|
)
|
||||||
|
elif msg["text"]:
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=chat_id,
|
||||||
|
text=msg["text"],
|
||||||
|
reply_markup=msg["keyboard"]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"[ASYNC] Сообщение '{event}' отправлено пользователю {chat_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[ASYNC] Ошибка Telegram при отправке '{event}' пользователю {chat_id}: {e}")
|
||||||
|
|||||||
BIN
lottery/bot_messages/photo_2025-07-10_09-33-13.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
lottery/bot_messages/photo_2025-07-10_09-33-22.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
lottery/bot_messages/photo_2025-07-10_09-33-33.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
lottery/bot_messages/photo_2025-07-10_09-33-35.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
@@ -1,23 +1,25 @@
|
|||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.urls import path, reverse
|
from django.urls import path, reverse
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.utils.dateparse import parse_date
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
|
|
||||||
from .models import Lottery, Prize, LotteryParticipant, DrawResult
|
from .models import Lottery, Prize, LotteryParticipant, DrawResult
|
||||||
from .forms import AddParticipantsForm
|
from .forms import AddParticipantsForm
|
||||||
from webapp.models import Invoice, Client, BindingRequest
|
from webapp.models import Invoice, Client, BindingRequest
|
||||||
from bot.notifications import NotificationService
|
from bot.notifications import NotificationService
|
||||||
from bot.utils import create_bot_instance
|
from bot.utils import create_bot_instance
|
||||||
from .views import view_draw_results
|
from .views import view_draw_results
|
||||||
from django import forms
|
import os
|
||||||
from django.utils.dateparse import parse_datetime, parse_date
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Настройка логгера
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
if not logger.handlers:
|
if not logger.handlers:
|
||||||
@@ -34,7 +36,6 @@ def add_participants_view(request):
|
|||||||
return HttpResponse("Не указан параметр lottery_id", status=400)
|
return HttpResponse("Не указан параметр lottery_id", status=400)
|
||||||
lottery = get_object_or_404(Lottery, id=lottery_id)
|
lottery = get_object_or_404(Lottery, id=lottery_id)
|
||||||
|
|
||||||
# Только счета, у которых used=False, и не добавленные в данную лотерею
|
|
||||||
used_invoice_ids = LotteryParticipant.objects.filter(lottery=lottery).values_list("invoice_id", flat=True)
|
used_invoice_ids = LotteryParticipant.objects.filter(lottery=lottery).values_list("invoice_id", flat=True)
|
||||||
qs = Invoice.objects.filter(used=False).exclude(id__in=used_invoice_ids)
|
qs = Invoice.objects.filter(used=False).exclude(id__in=used_invoice_ids)
|
||||||
|
|
||||||
@@ -47,7 +48,6 @@ def add_participants_view(request):
|
|||||||
qs = qs.filter(deposit_sum__gte=deposit_min)
|
qs = qs.filter(deposit_sum__gte=deposit_min)
|
||||||
if deposit_max:
|
if deposit_max:
|
||||||
qs = qs.filter(deposit_sum__lte=deposit_max)
|
qs = qs.filter(deposit_sum__lte=deposit_max)
|
||||||
|
|
||||||
if created_after:
|
if created_after:
|
||||||
try:
|
try:
|
||||||
dt = parse_date(created_after)
|
dt = parse_date(created_after)
|
||||||
@@ -55,7 +55,6 @@ def add_participants_view(request):
|
|||||||
qs = qs.filter(created_at__gte=dt)
|
qs = qs.filter(created_at__gte=dt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if created_before:
|
if created_before:
|
||||||
try:
|
try:
|
||||||
dt = parse_date(created_before)
|
dt = parse_date(created_before)
|
||||||
@@ -63,14 +62,13 @@ def add_participants_view(request):
|
|||||||
qs = qs.filter(created_at__lte=dt)
|
qs = qs.filter(created_at__lte=dt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = AddParticipantsForm(request.POST)
|
form = AddParticipantsForm(request.POST)
|
||||||
form.fields["invoices"].queryset = qs
|
form.fields["invoices"].queryset = qs
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
selected_invoices = form.cleaned_data["invoices"]
|
selected_invoices = form.cleaned_data["invoices"]
|
||||||
for invoice in selected_invoices:
|
for invoice in selected_invoices:
|
||||||
# При добавлении участника отмечаем счет как использованный
|
|
||||||
invoice.used = True
|
invoice.used = True
|
||||||
invoice.save()
|
invoice.save()
|
||||||
LotteryParticipant.objects.create(lottery=lottery, invoice=invoice)
|
LotteryParticipant.objects.create(lottery=lottery, invoice=invoice)
|
||||||
@@ -85,16 +83,90 @@ def add_participants_view(request):
|
|||||||
|
|
||||||
|
|
||||||
def get_client_by_invoice(invoice):
|
def get_client_by_invoice(invoice):
|
||||||
"""
|
|
||||||
Возвращает клиента, используя значение поля client_club_card_number у счета.
|
|
||||||
Предполагается, что у модели Invoice есть поле client_club_card_number.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
return Client.objects.get(club_card_number=invoice.client_club_card_number)
|
return Client.objects.get(club_card_number=invoice.client_club_card_number)
|
||||||
except Client.DoesNotExist:
|
except Client.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# def start_draw(request, lottery_id):
|
||||||
|
# lottery = get_object_or_404(Lottery, id=lottery_id)
|
||||||
|
# logger.info("Запуск розыгрыша для лотереи: %s", lottery.name)
|
||||||
|
|
||||||
|
# if lottery.finished:
|
||||||
|
# messages.warning(request, "Розыгрыш уже завершён.")
|
||||||
|
# return redirect("..")
|
||||||
|
|
||||||
|
# notifier = NotificationService(bot=create_bot_instance())
|
||||||
|
# async_to_sync(notifier.notify_draw_start)(lottery)
|
||||||
|
|
||||||
|
# manually_assigned_invoice_ids = set()
|
||||||
|
# for prize in lottery.prizes.all():
|
||||||
|
# if prize.winner and prize.winner.invoice:
|
||||||
|
# manually_assigned_invoice_ids.add(prize.winner.invoice_id)
|
||||||
|
# prize.winner.used = True
|
||||||
|
# prize.winner.save()
|
||||||
|
|
||||||
|
# for prize in lottery.prizes.all():
|
||||||
|
# logger.info("Обработка приза: %s", prize.prize_place)
|
||||||
|
|
||||||
|
# if prize.winner:
|
||||||
|
# try:
|
||||||
|
# draw_result = lottery.draw_results.get(prize=prize)
|
||||||
|
# draw_result.participant = prize.winner
|
||||||
|
# draw_result.drawn_at = timezone.now()
|
||||||
|
# draw_result.confirmed = False
|
||||||
|
# draw_result.save()
|
||||||
|
# except DrawResult.DoesNotExist:
|
||||||
|
# DrawResult.objects.create(
|
||||||
|
# lottery=lottery,
|
||||||
|
# prize=prize,
|
||||||
|
# participant=prize.winner,
|
||||||
|
# confirmed=False,
|
||||||
|
# drawn_at=timezone.now()
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# draw_result = lottery.draw_results.get(prize=prize)
|
||||||
|
# if draw_result.confirmed:
|
||||||
|
# continue
|
||||||
|
# except DrawResult.DoesNotExist:
|
||||||
|
# draw_result = None
|
||||||
|
|
||||||
|
# participants = list(
|
||||||
|
# lottery.participants.filter(used=False).exclude(invoice_id__in=manually_assigned_invoice_ids)
|
||||||
|
# )
|
||||||
|
# if not participants:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# winner_participant = random.choice(participants)
|
||||||
|
# winner_participant.used = True
|
||||||
|
# winner_participant.save()
|
||||||
|
|
||||||
|
# if draw_result:
|
||||||
|
# draw_result.participant = winner_participant
|
||||||
|
# draw_result.drawn_at = timezone.now()
|
||||||
|
# draw_result.confirmed = False
|
||||||
|
# draw_result.save()
|
||||||
|
# else:
|
||||||
|
# DrawResult.objects.create(
|
||||||
|
# lottery=lottery,
|
||||||
|
# prize=prize,
|
||||||
|
# participant=winner_participant,
|
||||||
|
# confirmed=False,
|
||||||
|
# drawn_at=timezone.now()
|
||||||
|
# )
|
||||||
|
|
||||||
|
# draw_results = lottery.draw_results.all()
|
||||||
|
# async_to_sync(notifier.notify_draw_results)(lottery, draw_results)
|
||||||
|
|
||||||
|
# if not lottery.prizes.filter(winner__isnull=True).exists():
|
||||||
|
# lottery.finished = True
|
||||||
|
# lottery.save()
|
||||||
|
|
||||||
|
# return render(request, "admin/draw_result.html", {"lottery": lottery, "draw_results": draw_results})
|
||||||
|
|
||||||
def start_draw(request, lottery_id):
|
def start_draw(request, lottery_id):
|
||||||
lottery = get_object_or_404(Lottery, id=lottery_id)
|
lottery = get_object_or_404(Lottery, id=lottery_id)
|
||||||
logger.info("Запуск розыгрыша для лотереи: %s", lottery.name)
|
logger.info("Запуск розыгрыша для лотереи: %s", lottery.name)
|
||||||
@@ -104,23 +176,18 @@ def start_draw(request, lottery_id):
|
|||||||
return redirect("..")
|
return redirect("..")
|
||||||
|
|
||||||
notifier = NotificationService(bot=create_bot_instance())
|
notifier = NotificationService(bot=create_bot_instance())
|
||||||
async_to_sync(notifier.notify_draw_start)(lottery)
|
|
||||||
|
|
||||||
# Собираем ID счетов вручную назначенных победителей
|
|
||||||
manually_assigned_invoice_ids = set()
|
manually_assigned_invoice_ids = set()
|
||||||
for prize in lottery.prizes.all():
|
for prize in lottery.prizes.all():
|
||||||
if prize.winner and prize.winner.invoice:
|
if prize.winner and prize.winner.invoice:
|
||||||
manually_assigned_invoice_ids.add(prize.winner.invoice_id)
|
manually_assigned_invoice_ids.add(prize.winner.invoice_id)
|
||||||
# Помечаем вручную выбранного участника как использованного
|
|
||||||
prize.winner.used = True
|
prize.winner.used = True
|
||||||
prize.winner.save()
|
prize.winner.save()
|
||||||
|
|
||||||
for prize in lottery.prizes.all():
|
for prize in lottery.prizes.all():
|
||||||
logger.info("Обработка приза: %s", prize.prize_place)
|
logger.info("Обработка приза: %s", prize.prize_place)
|
||||||
|
|
||||||
# Если у приза уже установлен победитель вручную — сохраняем его в таблицу результатов
|
|
||||||
if prize.winner:
|
if prize.winner:
|
||||||
logger.info("Приз '%s' имеет установленного вручную победителя. Сохраняем в таблице результатов.", prize.prize_place)
|
|
||||||
try:
|
try:
|
||||||
draw_result = lottery.draw_results.get(prize=prize)
|
draw_result = lottery.draw_results.get(prize=prize)
|
||||||
draw_result.participant = prize.winner
|
draw_result.participant = prize.winner
|
||||||
@@ -140,31 +207,26 @@ def start_draw(request, lottery_id):
|
|||||||
try:
|
try:
|
||||||
draw_result = lottery.draw_results.get(prize=prize)
|
draw_result = lottery.draw_results.get(prize=prize)
|
||||||
if draw_result.confirmed:
|
if draw_result.confirmed:
|
||||||
logger.info("Приз '%s' уже подтвержден.", prize.prize_place)
|
|
||||||
continue
|
continue
|
||||||
except DrawResult.DoesNotExist:
|
except DrawResult.DoesNotExist:
|
||||||
draw_result = None
|
draw_result = None
|
||||||
|
|
||||||
# Получаем всех неиспользованных участников, исключая вручную выбранных
|
|
||||||
participants = list(
|
participants = list(
|
||||||
lottery.participants.filter(used=False).exclude(invoice_id__in=manually_assigned_invoice_ids)
|
lottery.participants.filter(used=False).exclude(invoice_id__in=manually_assigned_invoice_ids)
|
||||||
)
|
)
|
||||||
logger.info("Найдено свободных участников для приза '%s': %d", prize.prize_place, len(participants))
|
logger.info(f"Найдено участников: {len(participants)} для приза {prize.prize_place}")
|
||||||
if not participants:
|
if not participants:
|
||||||
logger.warning("Нет свободных участников для приза '%s'.", prize.prize_place)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
winner_participant = random.choice(participants)
|
winner_participant = random.choice(participants)
|
||||||
winner_participant.used = True
|
winner_participant.used = True
|
||||||
winner_participant.save()
|
winner_participant.save()
|
||||||
logger.info("Выбран участник с счетом '%s' для приза '%s'.", winner_participant.invoice.api_id, prize.prize_place)
|
|
||||||
|
|
||||||
if draw_result:
|
if draw_result:
|
||||||
draw_result.participant = winner_participant
|
draw_result.participant = winner_participant
|
||||||
draw_result.drawn_at = timezone.now()
|
draw_result.drawn_at = timezone.now()
|
||||||
draw_result.confirmed = False
|
draw_result.confirmed = False
|
||||||
draw_result.save()
|
draw_result.save()
|
||||||
logger.info("Обновлен результат розыгрыша для приза '%s'.", prize.prize_place)
|
|
||||||
else:
|
else:
|
||||||
DrawResult.objects.create(
|
DrawResult.objects.create(
|
||||||
lottery=lottery,
|
lottery=lottery,
|
||||||
@@ -173,10 +235,30 @@ def start_draw(request, lottery_id):
|
|||||||
confirmed=False,
|
confirmed=False,
|
||||||
drawn_at=timezone.now()
|
drawn_at=timezone.now()
|
||||||
)
|
)
|
||||||
logger.info("Создан результат розыгрыша для приза '%s'.", prize.prize_place)
|
|
||||||
|
|
||||||
draw_results = lottery.draw_results.all()
|
draw_results = lottery.draw_results.select_related('prize', 'participant__invoice')
|
||||||
async_to_sync(notifier.notify_draw_results)(lottery, draw_results)
|
|
||||||
|
# 🧠 Считаем сумму призов и формируем список победителей
|
||||||
|
total_reward = sum([r.prize.reward for r in draw_results if r.prize and r.prize.reward])
|
||||||
|
winners_list = ""
|
||||||
|
for r in draw_results:
|
||||||
|
try:
|
||||||
|
winners_list += f"• Счёт {r.participant.invoice.id} — {r.prize.reward}₩\\n"
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
async def notify_all():
|
||||||
|
await notifier.notify_draw_start(lottery)
|
||||||
|
await notifier.notify_draw_results(
|
||||||
|
lottery,
|
||||||
|
results=draw_results,
|
||||||
|
context_vars={
|
||||||
|
"total_reward": f"{total_reward:,}",
|
||||||
|
"winners_list": winners_list.strip()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async_to_sync(notify_all)()
|
||||||
|
|
||||||
if not lottery.prizes.filter(winner__isnull=True).exists():
|
if not lottery.prizes.filter(winner__isnull=True).exists():
|
||||||
lottery.finished = True
|
lottery.finished = True
|
||||||
@@ -185,15 +267,14 @@ def start_draw(request, lottery_id):
|
|||||||
return render(request, "admin/draw_result.html", {"lottery": lottery, "draw_results": draw_results})
|
return render(request, "admin/draw_result.html", {"lottery": lottery, "draw_results": draw_results})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def confirm_draw_result(request, result_id):
|
def confirm_draw_result(request, result_id):
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
result = get_object_or_404(DrawResult, id=result_id)
|
result = get_object_or_404(DrawResult, id=result_id)
|
||||||
|
|
||||||
# Проверяем, что для результата задан участник и его счет
|
|
||||||
if not result.participant or not result.participant.invoice:
|
if not result.participant or not result.participant.invoice:
|
||||||
messages.error(request, "Невозможно подтвердить результат: отсутствует участник или его счет.")
|
messages.error(request, "Невозможно подтвердить результат: отсутствует участник или его счет.")
|
||||||
return HttpResponseRedirect(reverse("admin:start_draw", args=[result.lottery.id]))
|
return HttpResponseRedirect(reverse("admin:start_draw", args=[result.lottery.id]))
|
||||||
|
|
||||||
result.confirmed = True
|
result.confirmed = True
|
||||||
result.save()
|
result.save()
|
||||||
|
|
||||||
@@ -201,18 +282,14 @@ def confirm_draw_result(request, result_id):
|
|||||||
prize.winner = result.participant
|
prize.winner = result.participant
|
||||||
prize.save()
|
prize.save()
|
||||||
|
|
||||||
logger.info("Подтвержден результат розыгрыша для приза '%s'.", prize.prize_place)
|
|
||||||
messages.success(request, f"Результат для приза '{prize.prize_place}' подтвержден.")
|
|
||||||
|
|
||||||
# Получаем клиента по счету участника
|
|
||||||
client = get_client_by_invoice(result.participant.invoice)
|
client = get_client_by_invoice(result.participant.invoice)
|
||||||
if client:
|
if client:
|
||||||
try:
|
try:
|
||||||
# Можно попробовать запуск уведомления в отдельном потоке или отключить его временно:
|
|
||||||
async_to_sync(NotificationService(bot=create_bot_instance()).notify_prize_status_update)(client, result)
|
async_to_sync(NotificationService(bot=create_bot_instance()).notify_prize_status_update)(client, result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка отправки уведомления о статусе приза пользователю {client.telegram_id}: {e}")
|
logger.error(f"Ошибка отправки уведомления о статусе приза пользователю {client.telegram_id}: {e}")
|
||||||
|
|
||||||
|
messages.success(request, f"Результат для приза '{prize.prize_place}' подтвержден.")
|
||||||
return HttpResponseRedirect(reverse("admin:view_draw_results", args=[result.lottery.id]))
|
return HttpResponseRedirect(reverse("admin:view_draw_results", args=[result.lottery.id]))
|
||||||
|
|
||||||
|
|
||||||
@@ -227,16 +304,15 @@ class LotteryAdmin(admin.ModelAdmin):
|
|||||||
path('<int:lottery_id>/start-draw/', self.admin_site.admin_view(start_draw), name='start_draw'),
|
path('<int:lottery_id>/start-draw/', self.admin_site.admin_view(start_draw), name='start_draw'),
|
||||||
path('confirm-draw-result/<int:result_id>/', self.admin_site.admin_view(confirm_draw_result), name='confirm_draw_result'),
|
path('confirm-draw-result/<int:result_id>/', self.admin_site.admin_view(confirm_draw_result), name='confirm_draw_result'),
|
||||||
path('<int:lottery_id>/view-draw-results/', self.admin_site.admin_view(view_draw_results), name='view_draw_results'),
|
path('<int:lottery_id>/view-draw-results/', self.admin_site.admin_view(view_draw_results), name='view_draw_results'),
|
||||||
|
|
||||||
]
|
]
|
||||||
return custom_urls + urls
|
return custom_urls + urls
|
||||||
|
|
||||||
def start_draw_button(self, obj):
|
def start_draw_button(self, obj):
|
||||||
# Если лотерея завершена, кнопку скрываем
|
|
||||||
if obj.finished:
|
if obj.finished:
|
||||||
return ""
|
return ""
|
||||||
url = reverse("admin:start_draw", args=[obj.pk])
|
url = reverse("admin:start_draw", args=[obj.pk])
|
||||||
return format_html('<a class="btn btn-primary" href="{}">Запуск розыгрыша</a>', url)
|
return format_html('<a class="btn btn-primary" href="{}">Запуск розыгрыша</a>', url)
|
||||||
|
|
||||||
start_draw_button.short_description = "Розыгрыш"
|
start_draw_button.short_description = "Розыгрыш"
|
||||||
|
|
||||||
|
|
||||||
@@ -248,7 +324,6 @@ class PrizeAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
form = super().get_form(request, obj, **kwargs)
|
form = super().get_form(request, obj, **kwargs)
|
||||||
# При создании нового приза поле winner скрываем, так как участников ещё нет
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
form.base_fields.pop('winner', None)
|
form.base_fields.pop('winner', None)
|
||||||
return form
|
return form
|
||||||
@@ -258,13 +333,12 @@ class PrizeAdmin(admin.ModelAdmin):
|
|||||||
obj_id = request.resolver_match.kwargs.get('object_id')
|
obj_id = request.resolver_match.kwargs.get('object_id')
|
||||||
try:
|
try:
|
||||||
prize_obj = self.model.objects.get(pk=obj_id)
|
prize_obj = self.model.objects.get(pk=obj_id)
|
||||||
# Ограничиваем выбор участниками данной лотереи
|
|
||||||
kwargs["queryset"] = prize_obj.lottery.participants.all()
|
kwargs["queryset"] = prize_obj.lottery.participants.all()
|
||||||
except self.model.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
kwargs["queryset"] = self.model.objects.none()
|
kwargs["queryset"] = self.model.objects.none()
|
||||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(LotteryParticipant)
|
@admin.register(LotteryParticipant)
|
||||||
class LotteryParticipantAdmin(admin.ModelAdmin):
|
class LotteryParticipantAdmin(admin.ModelAdmin):
|
||||||
list_display = ("lottery", "invoice", "added_at", "used")
|
list_display = ("lottery", "invoice", "added_at", "used")
|
||||||
@@ -274,13 +348,11 @@ class LotteryParticipantAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
from draw.models import Lottery
|
|
||||||
active_lotteries = Lottery.objects.filter(prizes__winner__isnull=True).distinct()
|
active_lotteries = Lottery.objects.filter(prizes__winner__isnull=True).distinct()
|
||||||
extra_context['active_lotteries'] = active_lotteries
|
extra_context['active_lotteries'] = active_lotteries
|
||||||
return super().changelist_view(request, extra_context=extra_context)
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
from django.urls import path
|
|
||||||
urls = super().get_urls()
|
urls = super().get_urls()
|
||||||
custom_urls = [
|
custom_urls = [
|
||||||
path('add-participants/', self.admin_site.admin_view(add_participants_view), name="add_participants"),
|
path('add-participants/', self.admin_site.admin_view(add_participants_view), name="add_participants"),
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
|||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
JAZZMIN_SETTINGS = {
|
JAZZMIN_SETTINGS = {
|
||||||
# Общие настройки
|
# Общие настройки
|
||||||
"site_title": "LuckyTicket",
|
"site_title": "LuckyTicket",
|
||||||
@@ -192,6 +197,8 @@ JAZZMIN_SETTINGS = {
|
|||||||
"icons": {
|
"icons": {
|
||||||
"bot" : "fa-solid fa-robot",
|
"bot" : "fa-solid fa-robot",
|
||||||
"bot.BotConfig" : "fa-solid fa-wrench",
|
"bot.BotConfig" : "fa-solid fa-wrench",
|
||||||
|
"bot.BotMessage" : "fas fa-envelope",
|
||||||
|
"bot.BotEventMessageConfig" : "fa-solid fa-screwdriver-wrench",
|
||||||
"bot.WelcomeMessage" : "fa-solid fa-message",
|
"bot.WelcomeMessage" : "fa-solid fa-message",
|
||||||
"auth": "fas fa-user",
|
"auth": "fas fa-user",
|
||||||
"auth.user": "fas fa-user",
|
"auth.user": "fas fa-user",
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ Including another URLconf
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', lambda request: redirect('admin:index')),
|
path('', lambda request: redirect('admin:index')),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('invoices/', include('webapp.urls'))
|
path('invoices/', include('webapp.urls'))
|
||||||
]
|
]
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
BIN
lottery/media/bot_messages/photo_2025-07-10_09-33-13.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
lottery/media/bot_messages/photo_2025-07-10_09-33-15.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
lottery/media/bot_messages/photo_2025-07-10_09-33-17_2.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
lottery/media/bot_messages/photo_2025-07-10_09-33-34.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
lottery/media/welcome_messages/photo_2025-07-10_09-33-33.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
@@ -17,11 +17,11 @@ sniffio==1.3.1
|
|||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.12.2
|
||||||
urllib3==2.3.0
|
urllib3==2.3.0
|
||||||
mysqlclient
|
|
||||||
celery
|
celery
|
||||||
redis
|
redis
|
||||||
docker
|
docker
|
||||||
django-celery-beat
|
django-celery-beat
|
||||||
django-celery-results
|
django-celery-results
|
||||||
django-redis
|
django-redis
|
||||||
flower
|
flower
|
||||||
|
mysqlclient
|
||||||
@@ -4,4 +4,4 @@ from django.apps import AppConfig
|
|||||||
class WebappConfig(AppConfig):
|
class WebappConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'webapp'
|
name = 'webapp'
|
||||||
verbose_name='Основноая информация'
|
verbose_name='Основная информация'
|
||||||
17
lottery/webapp/migrations/0009_alter_apisettings_options.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-08-03 05:05
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('webapp', '0008_invoice_used'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='apisettings',
|
||||||
|
options={'verbose_name': 'Настройка API', 'verbose_name_plural': 'Настройки API'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
class Client(models.Model):
|
class Client(models.Model):
|
||||||
name = models.CharField("Имя", max_length=255)
|
name = models.CharField("Имя", max_length=255)
|
||||||
club_card_number = models.CharField("Номер клубной карты", max_length=100, unique=True)
|
club_card_number = models.CharField("Номер клубной карты", max_length=100, unique=True)
|
||||||
@@ -57,10 +58,13 @@ class Invoice(models.Model):
|
|||||||
class APISettings(models.Model):
|
class APISettings(models.Model):
|
||||||
api_url = models.URLField("API URL")
|
api_url = models.URLField("API URL")
|
||||||
api_key = models.CharField("API KEY", max_length=255)
|
api_key = models.CharField("API KEY", max_length=255)
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Настройка API"
|
||||||
|
verbose_name_plural = "Настройки API"
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Настройки API: {self.api_url}"
|
return f"Настройки API: {self.api_url}"
|
||||||
|
|
||||||
class BindingRequest(models.Model):
|
class BindingRequest(models.Model):
|
||||||
STATUS_CHOICES = (
|
STATUS_CHOICES = (
|
||||||
('pending', 'Ожидает проверки'),
|
('pending', 'Ожидает проверки'),
|
||||||
|
|||||||