Initial commit

This commit is contained in:
2026-01-02 20:18:42 +03:00
parent b2b49fbe51
commit 3a70802256
14 changed files with 626 additions and 19 deletions
@@ -0,0 +1,38 @@
"""add group
Revision ID: 520eccd2e55f
Revises: d3bd5df63c1b
Create Date: 2026-01-02 19:42:09.264423
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
revision: str = '520eccd2e55f'
down_revision: str | None = 'd3bd5df63c1b'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('groups',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('number', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint('number >= 1000 AND number <= 9999', name='check_group_number'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_groups_number'), 'groups', ['number'], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_groups_number'), table_name='groups')
op.drop_table('groups')
# ### end Alembic commands ###
+4
View File
@@ -9,11 +9,13 @@ from dishka import make_async_container
from dishka.integrations.aiogram import setup_dishka from dishka.integrations.aiogram import setup_dishka
from trudex.application.bot.admin_dialogs.broadcast import broadcast_dialog as admin_broadcast_dialog from trudex.application.bot.admin_dialogs.broadcast import broadcast_dialog as admin_broadcast_dialog
from trudex.application.bot.admin_dialogs.groups import groups_dialog as admin_groups_dialog
from trudex.application.bot.admin_dialogs.main_menu import admin_menu_dialog from trudex.application.bot.admin_dialogs.main_menu import admin_menu_dialog
from trudex.application.bot.admin_dialogs.tests import tests_dialog as admin_tests_dialog from trudex.application.bot.admin_dialogs.tests import tests_dialog as admin_tests_dialog
from trudex.application.bot.admin_dialogs.users import users_dialog as admin_users_dialog from trudex.application.bot.admin_dialogs.users import users_dialog as admin_users_dialog
from trudex.application.bot.creator_dialogs.broadcast import broadcast_dialog as creator_broadcast_dialog from trudex.application.bot.creator_dialogs.broadcast import broadcast_dialog as creator_broadcast_dialog
from trudex.application.bot.creator_dialogs.create_test import create_test_dialog from trudex.application.bot.creator_dialogs.create_test import create_test_dialog
from trudex.application.bot.creator_dialogs.groups import groups_dialog as creator_groups_dialog
from trudex.application.bot.creator_dialogs.main_menu import creator_menu_dialog from trudex.application.bot.creator_dialogs.main_menu import creator_menu_dialog
from trudex.application.bot.creator_dialogs.tests import tests_dialog as creator_tests_dialog from trudex.application.bot.creator_dialogs.tests import tests_dialog as creator_tests_dialog
from trudex.application.bot.creator_dialogs.users import users_dialog as creator_users_dialog from trudex.application.bot.creator_dialogs.users import users_dialog as creator_users_dialog
@@ -48,10 +50,12 @@ async def main() -> None:
admin_menu_dialog, admin_menu_dialog,
admin_users_dialog, admin_users_dialog,
admin_tests_dialog, admin_tests_dialog,
admin_groups_dialog,
admin_broadcast_dialog, admin_broadcast_dialog,
creator_menu_dialog, creator_menu_dialog,
creator_users_dialog, creator_users_dialog,
creator_tests_dialog, creator_tests_dialog,
creator_groups_dialog,
creator_broadcast_dialog, creator_broadcast_dialog,
create_test_dialog, create_test_dialog,
) )
@@ -0,0 +1,193 @@
from aiogram.types import CallbackQuery, Message
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Button, Column, Row, ScrollingGroup, Select
from aiogram_dialog.widgets.text import Const, Format
from dishka.integrations.aiogram import CONTAINER_NAME
from trudex.application.bot.admin_dialogs.states import AdminGroupsSG, AdminMenuSG
from trudex.infrastructure.database.dao.group import GroupDAO
async def on_group_click(_callback: CallbackQuery, _widget, _manager: DialogManager, _item_id: str):
await _callback.answer("ℹ️ Для удаления используйте кнопку 'Удалить группу'")
async def get_groups_data(dialog_manager: DialogManager, **_kwargs):
container = dialog_manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
groups = await group_dao.get_all()
success_message = dialog_manager.dialog_data.pop("success_message", None)
message_text = "<b>👥 Управление группами</b>\n\n"
if success_message:
message_text += f"{success_message}\n\n"
message_text += f"📊 <b>Всего групп:</b> {len(groups)}\n\n<b>Список групп:</b>"
return {
"groups": [(str(g.id), str(g.number)) for g in groups],
"groups_count": len(groups),
"message_text": message_text,
}
async def on_add_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(AdminGroupsSG.add_group_input_number)
async def on_delete_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(AdminGroupsSG.delete_groups_list)
async def on_back_to_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
async def on_group_number_input(message: Message, _widget: MessageInput, manager: DialogManager):
if not message.text:
await message.answer("❌ Номер группы не может быть пустым")
return
number_str = message.text.strip()
if not number_str.isdigit():
await message.answer("❌ Номер группы должен содержать только цифры")
return
number = int(number_str)
if number < 1000 or number > 9999:
await message.answer("❌ Номер группы должен быть четырехзначным (1000-9999)")
return
container = manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
existing = await group_dao.get_by_number(number)
if existing:
await message.answer(f"❌ Группа с номером {number} уже существует")
return
try:
await group_dao.create(number=number)
manager.dialog_data["success_message"] = f"✅ Группа {number} создана"
except Exception as e:
await message.answer(f"❌ Ошибка создания группы: {e}")
return
await manager.switch_to(AdminGroupsSG.groups_list)
async def on_cancel_add(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(AdminGroupsSG.groups_list)
async def get_delete_groups_data(dialog_manager: DialogManager, **_kwargs):
container = dialog_manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
groups = await group_dao.get_all()
return {
"groups": [(str(g.id), f"{g.number}") for g in groups],
"groups_count": len(groups),
}
async def on_select_group_to_delete(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str):
container = manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
group = await group_dao.get_by_id(int(item_id))
if not group:
await _callback.answer("❌ Группа не найдена", show_alert=True)
return
manager.dialog_data["delete_group_id"] = group.id
manager.dialog_data["delete_group_number"] = group.number
await manager.switch_to(AdminGroupsSG.delete_confirm)
async def get_delete_confirm_data(dialog_manager: DialogManager, **_kwargs):
number = dialog_manager.dialog_data.get("delete_group_number", "")
return {
"group_info": str(number)
}
async def on_confirm_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager):
container = manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
group_id = manager.dialog_data.get("delete_group_id")
await group_dao.delete(group_id)
manager.dialog_data["success_message"] = "✅ Группа удалена"
await manager.switch_to(AdminGroupsSG.groups_list)
async def on_cancel_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(AdminGroupsSG.delete_groups_list)
groups_dialog = Dialog(
Window(
Format("{message_text}"),
ScrollingGroup(
Select(
Format("{item[1]}"),
id="groups",
item_id_getter=lambda x: x[0],
items="groups",
on_click=on_group_click,
),
id="groups_scroll",
width=2,
height=7,
),
Column(
Button(Const(" Добавить группу"), id="add", on_click=on_add_group),
Button(Const("🗑 Удалить группу"), id="delete", on_click=on_delete_group),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_menu),
),
state=AdminGroupsSG.groups_list,
getter=get_groups_data,
),
Window(
Const("<b>➕ Добавление группы</b>\n\n🔢 <b>Введите номер группы</b> (четырехзначное число 1000-9999):"),
MessageInput(on_group_number_input),
Button(Const("◀️ Отмена"), id="cancel", on_click=on_cancel_add),
state=AdminGroupsSG.add_group_input_number,
),
Window(
Format("<b>🗑 Удаление группы</b>\n\n<b>Выберите группу для удаления:</b>\n\n📊 <b>Всего групп:</b> {groups_count}"),
ScrollingGroup(
Select(
Format("{item[1]}"),
id="delete_groups",
item_id_getter=lambda x: x[0],
items="groups",
on_click=on_select_group_to_delete,
),
id="delete_groups_scroll",
width=2,
height=7,
),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_add),
state=AdminGroupsSG.delete_groups_list,
getter=get_delete_groups_data,
),
Window(
Format("<b>⚠️ Подтверждение удаления</b>\n\n<b>Точно хотите удалить группу?</b>\n\n👥 {group_info}"),
Row(
Button(Const("✅ Да, удалить"), id="confirm", on_click=on_confirm_delete),
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_delete),
),
state=AdminGroupsSG.delete_confirm,
getter=get_delete_confirm_data,
),
)
@@ -3,7 +3,7 @@ from aiogram_dialog import Dialog, DialogManager, Window, StartMode
from aiogram_dialog.widgets.kbd import Button, Column from aiogram_dialog.widgets.kbd import Button, Column
from aiogram_dialog.widgets.text import Const from aiogram_dialog.widgets.text import Const
from trudex.application.bot.admin_dialogs.states import AdminMenuSG, AdminUsersSG, AdminTestsSG, AdminBroadcastSG from trudex.application.bot.admin_dialogs.states import AdminMenuSG, AdminUsersSG, AdminTestsSG, AdminBroadcastSG, AdminGroupsSG
async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
@@ -14,6 +14,10 @@ async def on_users_clicked(_callback: CallbackQuery, _button: Button, manager: D
await manager.start(AdminUsersSG.users_list, mode=StartMode.RESET_STACK) await manager.start(AdminUsersSG.users_list, mode=StartMode.RESET_STACK)
async def on_groups_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
await manager.start(AdminGroupsSG.groups_list, mode=StartMode.RESET_STACK)
async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
await manager.start(AdminBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK) await manager.start(AdminBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK)
@@ -24,6 +28,7 @@ admin_menu_dialog = Dialog(
Column( Column(
Button(Const("📝 Тесты"), id="tests", on_click=on_tests_clicked), Button(Const("📝 Тесты"), id="tests", on_click=on_tests_clicked),
Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked), Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked),
Button(Const("🎓 Группы"), id="groups", on_click=on_groups_clicked),
Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked), Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked),
), ),
state=AdminMenuSG.main, state=AdminMenuSG.main,
@@ -18,3 +18,10 @@ class AdminTestsSG(StatesGroup):
class AdminBroadcastSG(StatesGroup): class AdminBroadcastSG(StatesGroup):
broadcast_input = State() broadcast_input = State()
broadcast_confirm = State() broadcast_confirm = State()
class AdminGroupsSG(StatesGroup):
groups_list = State()
add_group_input_number = State()
delete_groups_list = State()
delete_confirm = State()
@@ -3,12 +3,13 @@ from datetime import date, datetime
from aiogram.types import CallbackQuery, ContentType, Message from aiogram.types import CallbackQuery, ContentType, Message
from aiogram_dialog import Dialog, DialogManager, Window, StartMode from aiogram_dialog import Dialog, DialogManager, Window, StartMode
from aiogram_dialog.widgets.input import MessageInput from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Button, Calendar, Cancel, Column, Row, Select from aiogram_dialog.widgets.kbd import Button, Calendar, Cancel, Column, Row, ScrollingGroup, Select
from aiogram_dialog.widgets.text import Const, Format from aiogram_dialog.widgets.text import Const, Format
from dishka.integrations.aiogram import CONTAINER_NAME from dishka.integrations.aiogram import CONTAINER_NAME
from dishka.integrations.aiogram_dialog import inject from dishka.integrations.aiogram_dialog import inject
from trudex.application.bot.creator_dialogs.states import CreateTestSG, CreatorTestsSG from trudex.application.bot.creator_dialogs.states import CreateTestSG, CreatorTestsSG
from trudex.infrastructure.database.dao.group import GroupDAO
from trudex.infrastructure.database.dao.option import OptionDAO from trudex.infrastructure.database.dao.option import OptionDAO
from trudex.infrastructure.database.dao.question import QuestionDAO from trudex.infrastructure.database.dao.question import QuestionDAO
from trudex.infrastructure.database.dao.test import TestDAO from trudex.infrastructure.database.dao.test import TestDAO
@@ -66,11 +67,28 @@ async def on_password_input(message: Message, _widget: MessageInput, manager: Di
return return
manager.dialog_data["password"] = password manager.dialog_data["password"] = password
container = manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
groups = await group_dao.get_all()
if len(groups) == 0:
manager.dialog_data["for_group"] = None
await manager.switch_to(CreateTestSG.confirm_test_info)
else:
await manager.switch_to(CreateTestSG.input_expires_at) await manager.switch_to(CreateTestSG.input_expires_at)
async def on_skip_password(_callback: CallbackQuery, _button: Button, manager: DialogManager): async def on_skip_password(_callback: CallbackQuery, _button: Button, manager: DialogManager):
manager.dialog_data["password"] = None manager.dialog_data["password"] = None
container = manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
groups = await group_dao.get_all()
if len(groups) == 0:
manager.dialog_data["for_group"] = None
await manager.switch_to(CreateTestSG.confirm_test_info)
else:
await manager.switch_to(CreateTestSG.input_expires_at) await manager.switch_to(CreateTestSG.input_expires_at)
@@ -84,13 +102,19 @@ async def on_skip_expires(_callback: CallbackQuery, _button: Button, manager: Di
await manager.switch_to(CreateTestSG.input_for_group) await manager.switch_to(CreateTestSG.input_for_group)
async def on_group_input(message: Message, _widget: MessageInput, manager: DialogManager): async def get_groups_for_test(dialog_manager: DialogManager, **_kwargs):
text = (message.text or "").strip() container = dialog_manager.middleware_data[CONTAINER_NAME]
if text.isdigit() and len(text) == 4: group_dao = await container.get(GroupDAO)
manager.dialog_data["for_group"] = int(text) groups = await group_dao.get_all()
return {
"groups": [(str(g.number), str(g.number)) for g in groups],
}
async def on_group_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str):
manager.dialog_data["for_group"] = int(item_id)
await manager.switch_to(CreateTestSG.confirm_test_info) await manager.switch_to(CreateTestSG.confirm_test_info)
else:
await message.answer("❌ Группа должна быть 4-значным числом")
async def on_skip_group(_callback: CallbackQuery, _button: Button, manager: DialogManager): async def on_skip_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
@@ -183,7 +207,7 @@ async def get_question_type_data(**_kwargs):
return { return {
"question_types": [ "question_types": [
("single", "📌 Один правильный ответ"), ("single", "📌 Один правильный ответ"),
("multiple", " Ннесколько правильных ответов"), ("multiple", " Несколько правильных ответов"),
("input", "✏️ Ввод текста"), ("input", "✏️ Ввод текста"),
] ]
} }
@@ -423,10 +447,22 @@ create_test_dialog = Dialog(
state=CreateTestSG.input_expires_at, state=CreateTestSG.input_expires_at,
), ),
Window( Window(
Const("<b>👥 Группа</b>\n\n🎓 <b>Введите номер группы</b> (4 цифры) или пропустите для всех:"), Const("<b>👥 Группа</b>\n\n🎓 <b>Выберите группу</b> или пропустите для всех:"),
MessageInput(on_group_input), ScrollingGroup(
Select(
Format("{item[1]}"),
id="groups",
item_id_getter=lambda x: x[0],
items="groups",
on_click=on_group_selected,
),
id="groups_scroll",
width=2,
height=7,
),
Button(Const("⏭️ Для всех"), id="skip_group", on_click=on_skip_group), Button(Const("⏭️ Для всех"), id="skip_group", on_click=on_skip_group),
state=CreateTestSG.input_for_group, state=CreateTestSG.input_for_group,
getter=get_groups_for_test,
), ),
Window( Window(
Format("{info}\n\n<b>✅ Подтвердите создание теста:</b>"), Format("{info}\n\n<b>✅ Подтвердите создание теста:</b>"),
@@ -454,13 +490,13 @@ create_test_dialog = Dialog(
), ),
Window( Window(
Const("<b>📋 Тип вопроса</b>\n\n🎯 <b>Выберите тип вопроса:</b>"), Const("<b>📋 Тип вопроса</b>\n\n🎯 <b>Выберите тип вопроса:</b>"),
Select( Column(Select(
Format("{item[1]}"), Format("{item[1]}"),
id="question_type", id="question_type",
item_id_getter=lambda x: x[0], item_id_getter=lambda x: x[0],
items="question_types", items="question_types",
on_click=on_question_type_selected, on_click=on_question_type_selected,
), )),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question), Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
state=CreateTestSG.select_question_type, state=CreateTestSG.select_question_type,
getter=get_question_type_data, getter=get_question_type_data,
@@ -481,13 +517,13 @@ create_test_dialog = Dialog(
), ),
Window( Window(
Const("<b>✅ Правильные ответы</b>\n\n<b>Отметьте правильные варианты ответов:</b>"), Const("<b>✅ Правильные ответы</b>\n\n<b>Отметьте правильные варианты ответов:</b>"),
Select( Column(Select(
Format("{item[1]}"), Format("{item[1]}"),
id="options", id="options",
item_id_getter=lambda x: x[0], item_id_getter=lambda x: x[0],
items="options", items="options",
on_click=on_option_toggle, on_click=on_option_toggle,
), )),
Button(Const("✅ Подтвердить выбор"), id="confirm", on_click=on_confirm_correct), Button(Const("✅ Подтвердить выбор"), id="confirm", on_click=on_confirm_correct),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question), Button(Const("◀️ Назад"), id="back", on_click=on_cancel_question),
state=CreateTestSG.mark_correct_options, state=CreateTestSG.mark_correct_options,
@@ -0,0 +1,194 @@
from aiogram.types import CallbackQuery, Message
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Button, Column, Row, ScrollingGroup, Select
from aiogram_dialog.widgets.text import Const, Format
from dishka.integrations.aiogram import CONTAINER_NAME
from trudex.application.bot.creator_dialogs.states import CreatorGroupsSG
from trudex.infrastructure.database.dao.group import GroupDAO
async def on_group_click(_callback: CallbackQuery, _widget, _manager: DialogManager, _item_id: str):
await _callback.answer("ℹ️ Для удаления используйте кнопку 'Удалить группу'")
async def get_groups_data(dialog_manager: DialogManager, **_kwargs):
container = dialog_manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
groups = await group_dao.get_all()
success_message = dialog_manager.dialog_data.pop("success_message", None)
message_text = "<b>👥 Управление группами</b>\n\n"
if success_message:
message_text += f"{success_message}\n\n"
message_text += f"📊 <b>Всего групп:</b> {len(groups)}\n\n<b>Список групп:</b>"
return {
"groups": [(str(g.id), str(g.number)) for g in groups],
"groups_count": len(groups),
"message_text": message_text,
}
async def on_add_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(CreatorGroupsSG.add_group_input_number)
async def on_delete_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(CreatorGroupsSG.delete_groups_list)
async def on_back_to_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
from trudex.application.bot.creator_dialogs.states import CreatorMenuSG
await manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
async def on_group_number_input(message: Message, _widget: MessageInput, manager: DialogManager):
if not message.text:
await message.answer("❌ Номер группы не может быть пустым")
return
number_str = message.text.strip()
if not number_str.isdigit():
await message.answer("❌ Номер группы должен содержать только цифры")
return
number = int(number_str)
if number < 1000 or number > 9999:
await message.answer("❌ Номер группы должен быть четырехзначным (1000-9999)")
return
container = manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
existing = await group_dao.get_by_number(number)
if existing:
await message.answer(f"❌ Группа с номером {number} уже существует")
return
try:
await group_dao.create(number=number)
manager.dialog_data["success_message"] = f"✅ Группа {number} создана"
except Exception as e:
await message.answer(f"❌ Ошибка создания группы: {e}")
return
await manager.switch_to(CreatorGroupsSG.groups_list)
async def on_cancel_add(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(CreatorGroupsSG.groups_list)
async def get_delete_groups_data(dialog_manager: DialogManager, **_kwargs):
container = dialog_manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
groups = await group_dao.get_all()
return {
"groups": [(str(g.id), str(g.number)) for g in groups],
"groups_count": len(groups),
}
async def on_select_group_to_delete(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str):
container = manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
group = await group_dao.get_by_id(int(item_id))
if not group:
await _callback.answer("❌ Группа не найдена", show_alert=True)
return
manager.dialog_data["delete_group_id"] = group.id
manager.dialog_data["delete_group_number"] = group.number
await manager.switch_to(CreatorGroupsSG.delete_confirm)
async def get_delete_confirm_data(dialog_manager: DialogManager, **_kwargs):
number = dialog_manager.dialog_data.get("delete_group_number", "")
return {
"group_info": str(number)
}
async def on_confirm_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager):
container = manager.middleware_data[CONTAINER_NAME]
group_dao = await container.get(GroupDAO)
group_id = manager.dialog_data.get("delete_group_id")
await group_dao.delete(group_id)
manager.dialog_data["success_message"] = "✅ Группа удалена"
await manager.switch_to(CreatorGroupsSG.groups_list)
async def on_cancel_delete(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(CreatorGroupsSG.delete_groups_list)
groups_dialog = Dialog(
Window(
Format("{message_text}"),
ScrollingGroup(
Select(
Format("{item[1]}"),
id="groups",
item_id_getter=lambda x: x[0],
items="groups",
on_click=on_group_click,
),
id="groups_scroll",
width=2,
height=7,
),
Column(
Button(Const(" Добавить группу"), id="add", on_click=on_add_group),
Button(Const("🗑 Удалить группу"), id="delete", on_click=on_delete_group),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_menu),
),
state=CreatorGroupsSG.groups_list,
getter=get_groups_data,
),
Window(
Const("<b>➕ Добавление группы</b>\n\n🔢 <b>Введите номер группы</b> (четырехзначное число 1000-9999):"),
MessageInput(on_group_number_input),
Button(Const("◀️ Отмена"), id="cancel", on_click=on_cancel_add),
state=CreatorGroupsSG.add_group_input_number,
),
Window(
Format("<b>🗑 Удаление группы</b>\n\n<b>Выберите группу для удаления:</b>\n\n📊 <b>Всего групп:</b> {groups_count}"),
ScrollingGroup(
Select(
Format("{item[1]}"),
id="delete_groups",
item_id_getter=lambda x: x[0],
items="groups",
on_click=on_select_group_to_delete,
),
id="delete_groups_scroll",
width=2,
height=7,
),
Button(Const("◀️ Назад"), id="back", on_click=on_cancel_add),
state=CreatorGroupsSG.delete_groups_list,
getter=get_delete_groups_data,
),
Window(
Format("<b>⚠️ Подтверждение удаления</b>\n\n<b>Точно хотите удалить группу?</b>\n\n👥 {group_info}"),
Row(
Button(Const("✅ Да, удалить"), id="confirm", on_click=on_confirm_delete),
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_delete),
),
state=CreatorGroupsSG.delete_confirm,
getter=get_delete_confirm_data,
),
)
@@ -3,7 +3,7 @@ from aiogram_dialog import Dialog, DialogManager, Window, StartMode
from aiogram_dialog.widgets.kbd import Button, Column from aiogram_dialog.widgets.kbd import Button, Column
from aiogram_dialog.widgets.text import Const from aiogram_dialog.widgets.text import Const
from trudex.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorUsersSG, CreatorTestsSG, CreatorBroadcastSG from trudex.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorUsersSG, CreatorTestsSG, CreatorBroadcastSG, CreatorGroupsSG
async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
@@ -14,6 +14,10 @@ async def on_users_clicked(_callback: CallbackQuery, _button: Button, manager: D
await manager.start(CreatorUsersSG.users_list, mode=StartMode.RESET_STACK) await manager.start(CreatorUsersSG.users_list, mode=StartMode.RESET_STACK)
async def on_groups_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
await manager.start(CreatorGroupsSG.groups_list, mode=StartMode.RESET_STACK)
async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None: async def on_broadcast_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
await manager.start(CreatorBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK) await manager.start(CreatorBroadcastSG.broadcast_input, mode=StartMode.RESET_STACK)
@@ -24,6 +28,7 @@ creator_menu_dialog = Dialog(
Column( Column(
Button(Const("📝 Тесты"), id="tests", on_click=on_tests_clicked), Button(Const("📝 Тесты"), id="tests", on_click=on_tests_clicked),
Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked), Button(Const("👥 Пользователи"), id="users", on_click=on_users_clicked),
Button(Const("🎓 Группы"), id="groups", on_click=on_groups_clicked),
Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked), Button(Const("📢 Рассылка"), id="broadcast", on_click=on_broadcast_clicked),
), ),
state=CreatorMenuSG.main, state=CreatorMenuSG.main,
@@ -21,6 +21,13 @@ class CreatorBroadcastSG(StatesGroup):
broadcast_confirm = State() broadcast_confirm = State()
class CreatorGroupsSG(StatesGroup):
groups_list = State()
add_group_input_number = State()
delete_groups_list = State()
delete_confirm = State()
class CreateTestSG(StatesGroup): class CreateTestSG(StatesGroup):
input_title = State() input_title = State()
input_description = State() input_description = State()
+8
View File
@@ -14,6 +14,14 @@ class User:
updated_at: datetime | None = None updated_at: datetime | None = None
@dataclass
class Group:
id: int
number: int
created_at: datetime | None = None
updated_at: datetime | None = None
@dataclass @dataclass
class Test: class Test:
id: int id: int
@@ -0,0 +1,73 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from trudex.domain.schemas import Group as DomainGroup
from trudex.infrastructure.database.dto.group import GroupDTO
from trudex.infrastructure.database.models import Group
class GroupDAO:
def __init__(self, session: AsyncSession) -> None:
self.session: AsyncSession = session
async def get_by_id(self, group_id: int) -> DomainGroup | None:
result = await self.session.execute(
select(Group).where(Group.id == group_id)
)
model = result.scalar_one_or_none()
return GroupDTO(model).to_domain() if model else None
async def get_by_number(self, number: int) -> DomainGroup | None:
result = await self.session.execute(
select(Group).where(Group.number == number)
)
model = result.scalar_one_or_none()
return GroupDTO(model).to_domain() if model else None
async def get_all(self) -> list[DomainGroup]:
result = await self.session.execute(select(Group))
models = list(result.scalars().all())
return [GroupDTO(model).to_domain() for model in models]
async def create(
self,
number: int,
) -> DomainGroup:
group = Group(
number=number,
)
self.session.add(group)
await self.session.flush()
await self.session.refresh(group)
return GroupDTO(group).to_domain()
async def update(
self,
group_id: int,
number: int | None = None
) -> DomainGroup | None:
result = await self.session.execute(
select(Group).where(Group.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
return None
if number is not None:
group.number = number
await self.session.flush()
await self.session.refresh(group)
return GroupDTO(group).to_domain()
async def delete(self, group_id: int) -> bool:
result = await self.session.execute(
select(Group).where(Group.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
return False
await self.session.delete(group)
await self.session.flush()
return True
@@ -0,0 +1,15 @@
from trudex.domain.schemas import Group as DomainGroup
from trudex.infrastructure.database.models import Group as GroupModel
class GroupDTO:
def __init__(self, model: GroupModel) -> None:
self.model: GroupModel = model
def to_domain(self) -> DomainGroup:
return DomainGroup(
id=self.model.id,
number=self.model.number,
created_at=self.model.created_at,
updated_at=self.model.updated_at,
)
@@ -24,6 +24,23 @@ class User(Base):
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now()) updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
@final
class Group(Base):
__tablename__ = "groups"
id: Mapped[int] = mapped_column(primary_key=True)
number: Mapped[int] = mapped_column(Integer, unique=True, index=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
__table_args__ = (
CheckConstraint("number >= 1000 AND number <= 9999", name="check_group_number"),
)
__table_args__ = (
CheckConstraint("number >= 1000 AND number <= 9999", name="check_group_number"),
)
class QuestionType(str, Enum): class QuestionType(str, Enum):
SINGLE = "single" SINGLE = "single"
MULTIPLE = "multiple" MULTIPLE = "multiple"
+5
View File
@@ -4,6 +4,7 @@ from dishka import Provider, Scope, provide
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from trudex.infrastructure.database.config import new_session_maker from trudex.infrastructure.database.config import new_session_maker
from trudex.infrastructure.database.dao.group import GroupDAO
from trudex.infrastructure.database.dao.option import OptionDAO from trudex.infrastructure.database.dao.option import OptionDAO
from trudex.infrastructure.database.dao.question import QuestionDAO from trudex.infrastructure.database.dao.question import QuestionDAO
from trudex.infrastructure.database.dao.test import TestDAO from trudex.infrastructure.database.dao.test import TestDAO
@@ -37,6 +38,10 @@ class DatabaseProvider(Provider):
def get_user_dao(self, session: AsyncSession) -> UserDAO: def get_user_dao(self, session: AsyncSession) -> UserDAO:
return UserDAO(session) return UserDAO(session)
@provide(scope=Scope.REQUEST)
def get_group_dao(self, session: AsyncSession) -> GroupDAO:
return GroupDAO(session)
@provide(scope=Scope.REQUEST) @provide(scope=Scope.REQUEST)
def get_test_dao(self, session: AsyncSession) -> TestDAO: def get_test_dao(self, session: AsyncSession) -> TestDAO:
return TestDAO(session) return TestDAO(session)