mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 10:25:28 +03:00
commit
This commit is contained in:
@@ -9,42 +9,26 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from dishka import make_async_container
|
||||
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.create_test import \
|
||||
admin_create_test_dialog
|
||||
from trudex.application.bot.admin_dialogs.groups import \
|
||||
groups_dialog as admin_groups_dialog
|
||||
from trudex.application.bot.admin_dialogs.broadcast import broadcast_dialog as admin_broadcast_dialog
|
||||
from trudex.application.bot.admin_dialogs.create_test import admin_create_test_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.templates import \
|
||||
templates_dialog as admin_templates_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.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.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.templates import \
|
||||
templates_dialog as creator_templates_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.admin_dialogs.templates import templates_dialog as admin_templates_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.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.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.templates import templates_dialog as creator_templates_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.handlers import router
|
||||
from trudex.application.bot.middlewares.reject_not_admin import \
|
||||
RejectNotAdminMiddleware
|
||||
from trudex.application.bot.middlewares.reject_not_creator import \
|
||||
RejectNotCreatorMiddleware
|
||||
from trudex.application.bot.middlewares.reject_not_admin import RejectNotAdminMiddleware
|
||||
from trudex.application.bot.middlewares.reject_not_creator import RejectNotCreatorMiddleware
|
||||
from trudex.application.bot.user_dialogs.deeplink import deeplink_dialog
|
||||
from trudex.application.bot.user_dialogs.main_menu import user_menu_dialog
|
||||
from trudex.application.bot.user_dialogs.registration import \
|
||||
registration_dialog
|
||||
from trudex.application.bot.user_dialogs.registration import registration_dialog
|
||||
from trudex.application.bot.user_dialogs.take_test import take_test_dialog
|
||||
from trudex.infrastructure.database.repo.user import UserRepository
|
||||
from trudex.infrastructure.di import DatabaseProvider, SchedulerProvider
|
||||
@@ -99,8 +83,6 @@ async def main() -> None:
|
||||
)
|
||||
setup_dialogs(dp)
|
||||
setup_dishka(container, dp, auto_inject=True)
|
||||
|
||||
bott = await container.get(Bot)
|
||||
|
||||
async with container() as request_container:
|
||||
user_repo = await request_container.get(UserRepository)
|
||||
|
||||
@@ -6,8 +6,7 @@ from aiogram_dialog.widgets.text import Const
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.admin_dialogs.states import (AdminBroadcastSG,
|
||||
AdminMenuSG)
|
||||
from trudex.application.bot.admin_dialogs.states import AdminBroadcastSG, AdminMenuSG
|
||||
from trudex.infrastructure.database.dao.user import UserDAO
|
||||
from trudex.infrastructure.utils.broadcast import broadcast_message
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@ from datetime import date, datetime, time
|
||||
from aiogram.types import CallbackQuery, ContentType, Message
|
||||
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
||||
from aiogram_dialog.widgets.input import MessageInput
|
||||
from aiogram_dialog.widgets.kbd import (Button, Calendar, Cancel, Column, Row,
|
||||
ScrollingGroup, Select)
|
||||
from aiogram_dialog.widgets.kbd import Button, Calendar, Cancel, Column, Row, ScrollingGroup, Select
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.admin_dialogs.states import (AdminCreateTestSG,
|
||||
AdminTestsSG)
|
||||
from trudex.application.bot.admin_dialogs.states import AdminCreateTestSG, AdminTestsSG
|
||||
from trudex.infrastructure.database.dao.group import GroupDAO
|
||||
from trudex.infrastructure.database.dao.option import OptionDAO
|
||||
from trudex.infrastructure.database.dao.question import QuestionDAO
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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.kbd import Button, Column, Row, ScrollingGroup, Select
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.admin_dialogs.states import (AdminGroupsSG,
|
||||
AdminMenuSG)
|
||||
from trudex.application.bot.admin_dialogs.states import AdminGroupsSG, AdminMenuSG
|
||||
from trudex.infrastructure.database.dao.group import GroupDAO
|
||||
|
||||
|
||||
|
||||
@@ -3,12 +3,8 @@ from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
||||
from aiogram_dialog.widgets.kbd import Button, Column
|
||||
from aiogram_dialog.widgets.text import Const
|
||||
|
||||
from trudex.application.bot.admin_dialogs.states import (AdminBroadcastSG,
|
||||
AdminGroupsSG,
|
||||
AdminMenuSG,
|
||||
AdminTemplatesSG,
|
||||
AdminTestsSG,
|
||||
AdminUsersSG)
|
||||
from trudex.application.bot.admin_dialogs.states import (AdminBroadcastSG, AdminGroupsSG, AdminMenuSG,
|
||||
AdminTemplatesSG, AdminTestsSG, AdminUsersSG)
|
||||
|
||||
|
||||
async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
|
||||
|
||||
@@ -10,13 +10,13 @@ from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.admin_dialogs.states import AdminMenuSG, AdminTemplatesSG, AdminTestsSG
|
||||
from trudex.domain.schemas import QuestionType
|
||||
from trudex.domain.test_parser import ParsedTest, TestParser
|
||||
from trudex.infrastructure.database.dao.option import OptionDAO
|
||||
from trudex.infrastructure.database.dao.question import QuestionDAO
|
||||
from trudex.infrastructure.database.dao.test import TestDAO
|
||||
from trudex.infrastructure.database.repo.test import TestRepository
|
||||
|
||||
|
||||
TEMPLATES_INFO = (
|
||||
"<b>📦 Шаблоны тестов</b>\n\n"
|
||||
"Шаблоны позволяют экспортировать и импортировать тесты в формате JSON.\n\n"
|
||||
@@ -227,7 +227,7 @@ async def on_test_selected_for_export(
|
||||
"question_type": question.question_type,
|
||||
}
|
||||
|
||||
if question.question_type == "input":
|
||||
if question.question_type == QuestionType.INPUT:
|
||||
correct_options = [o for o in options if o.is_correct]
|
||||
if correct_options:
|
||||
question_data["correct_answer"] = correct_options[0].text
|
||||
|
||||
@@ -6,15 +6,12 @@ from aiogram import Bot
|
||||
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
||||
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
||||
from aiogram_dialog.widgets.input import MessageInput
|
||||
from aiogram_dialog.widgets.kbd import (Button, Calendar, Column,
|
||||
ScrollingGroup, Select)
|
||||
from aiogram_dialog.widgets.kbd import Button, Calendar, Column, ScrollingGroup, Select
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.admin_dialogs.states import (AdminMenuSG,
|
||||
AdminTestsSG,
|
||||
AdminCreateTestSG)
|
||||
from trudex.application.bot.admin_dialogs.states import AdminCreateTestSG, AdminMenuSG, AdminTestsSG
|
||||
from trudex.infrastructure.database.dao.group import GroupDAO
|
||||
from trudex.infrastructure.database.dao.test import TestDAO
|
||||
from trudex.infrastructure.database.repo.test import TestRepository
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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, ScrollingGroup, Select,
|
||||
SwitchTo)
|
||||
from aiogram_dialog.widgets.kbd import Button, Column, ScrollingGroup, Select, SwitchTo
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.admin_dialogs.states import (AdminMenuSG,
|
||||
AdminUsersSG)
|
||||
from trudex.application.bot.admin_dialogs.states import AdminMenuSG, AdminUsersSG
|
||||
from trudex.infrastructure.database.dao.user import UserDAO
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ from aiogram_dialog.widgets.text import Const
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG,
|
||||
CreatorMenuSG)
|
||||
from trudex.application.bot.creator_dialogs.states import CreatorBroadcastSG, CreatorMenuSG
|
||||
from trudex.infrastructure.database.dao.user import UserDAO
|
||||
from trudex.infrastructure.utils.broadcast import broadcast_message
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@ from datetime import date, datetime, time
|
||||
from aiogram.types import CallbackQuery, ContentType, Message
|
||||
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
||||
from aiogram_dialog.widgets.input import MessageInput
|
||||
from aiogram_dialog.widgets.kbd import (Button, Calendar, Cancel, Column, Row,
|
||||
ScrollingGroup, Select)
|
||||
from aiogram_dialog.widgets.kbd import Button, Calendar, Cancel, Column, Row, ScrollingGroup, Select
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
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.question import QuestionDAO
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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.kbd import Button, Column, Row, ScrollingGroup, Select
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.creator_dialogs.states import (CreatorGroupsSG,
|
||||
CreatorMenuSG)
|
||||
from trudex.application.bot.creator_dialogs.states import CreatorGroupsSG, CreatorMenuSG
|
||||
from trudex.infrastructure.database.dao.group import GroupDAO
|
||||
|
||||
|
||||
|
||||
@@ -3,12 +3,8 @@ from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
||||
from aiogram_dialog.widgets.kbd import Button, Column
|
||||
from aiogram_dialog.widgets.text import Const
|
||||
|
||||
from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG,
|
||||
CreatorGroupsSG,
|
||||
CreatorMenuSG,
|
||||
CreatorTemplatesSG,
|
||||
CreatorTestsSG,
|
||||
CreatorUsersSG)
|
||||
from trudex.application.bot.creator_dialogs.states import (CreatorBroadcastSG, CreatorGroupsSG, CreatorMenuSG,
|
||||
CreatorTemplatesSG, CreatorTestsSG, CreatorUsersSG)
|
||||
|
||||
|
||||
async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager) -> None:
|
||||
|
||||
@@ -10,13 +10,13 @@ from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorTemplatesSG, CreatorTestsSG
|
||||
from trudex.domain.schemas import QuestionType
|
||||
from trudex.domain.test_parser import ParsedTest, TestParser
|
||||
from trudex.infrastructure.database.dao.option import OptionDAO
|
||||
from trudex.infrastructure.database.dao.question import QuestionDAO
|
||||
from trudex.infrastructure.database.dao.test import TestDAO
|
||||
from trudex.infrastructure.database.repo.test import TestRepository
|
||||
|
||||
|
||||
TEMPLATES_INFO = (
|
||||
"<b>📦 Шаблоны тестов</b>\n\n"
|
||||
"Шаблоны позволяют экспортировать и импортировать тесты в формате JSON.\n\n"
|
||||
@@ -227,7 +227,7 @@ async def on_test_selected_for_export(
|
||||
"question_type": question.question_type,
|
||||
}
|
||||
|
||||
if question.question_type == "input":
|
||||
if question.question_type == QuestionType.INPUT:
|
||||
correct_options = [o for o in options if o.is_correct]
|
||||
if correct_options:
|
||||
question_data["correct_answer"] = correct_options[0].text
|
||||
|
||||
@@ -8,16 +8,13 @@ from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
||||
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
||||
from aiogram_dialog.api.entities import MediaAttachment
|
||||
from aiogram_dialog.widgets.input import MessageInput
|
||||
from aiogram_dialog.widgets.kbd import (Button, Calendar, Column, Row,
|
||||
ScrollingGroup, Select)
|
||||
from aiogram_dialog.widgets.kbd import Button, Calendar, Column, Row, ScrollingGroup, Select
|
||||
from aiogram_dialog.widgets.media import DynamicMedia
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.creator_dialogs.states import (CreateTestSG,
|
||||
CreatorMenuSG,
|
||||
CreatorTestsSG)
|
||||
from trudex.application.bot.creator_dialogs.states import CreateTestSG, CreatorMenuSG, CreatorTestsSG
|
||||
from trudex.infrastructure.database.dao.group import GroupDAO
|
||||
from trudex.infrastructure.database.dao.test import TestDAO
|
||||
from trudex.infrastructure.database.repo.test import TestRepository
|
||||
|
||||
@@ -4,14 +4,12 @@ from aiogram import Bot
|
||||
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, SwitchTo)
|
||||
from aiogram_dialog.widgets.kbd import Button, Column, Row, ScrollingGroup, Select, SwitchTo
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.creator_dialogs.states import (CreatorMenuSG,
|
||||
CreatorUsersSG)
|
||||
from trudex.application.bot.creator_dialogs.states import CreatorMenuSG, CreatorUsersSG
|
||||
from trudex.infrastructure.database.dao.user import UserDAO
|
||||
from trudex.infrastructure.database.repo.user import UserRepository
|
||||
from trudex.infrastructure.utils.bot_commands import setup_bot_commands
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import logging
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command, CommandStart, CommandObject
|
||||
from aiogram.filters import Command, CommandObject, CommandStart
|
||||
from aiogram.types import ErrorEvent, Message
|
||||
from aiogram_dialog import DialogManager, StartMode
|
||||
from aiogram_dialog.api.exceptions import OutdatedIntent, UnknownIntent
|
||||
@@ -7,11 +9,7 @@ from dishka.integrations.aiogram import FromDishka
|
||||
|
||||
from trudex.application.bot.admin_dialogs.states import AdminMenuSG
|
||||
from trudex.application.bot.creator_dialogs.states import CreatorMenuSG
|
||||
from trudex.application.bot.user_dialogs.states import (
|
||||
UserDeeplinkSG,
|
||||
UserMenuSG,
|
||||
UserRegistrationSG,
|
||||
)
|
||||
from trudex.application.bot.user_dialogs.states import UserDeeplinkSG, UserMenuSG, UserRegistrationSG
|
||||
from trudex.infrastructure.database.dao.group import GroupDAO
|
||||
from trudex.infrastructure.database.dao.test import TestDAO
|
||||
from trudex.infrastructure.database.dao.user import UserDAO
|
||||
@@ -20,6 +18,7 @@ from trudex.infrastructure.utils.test_id_to_hash import decode_id
|
||||
from trudex.infrastructure.utils.timezone import now_msk_naive
|
||||
|
||||
router = Router()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def ensure_user_registered(
|
||||
@@ -115,6 +114,13 @@ async def start_with_deeplink(
|
||||
assert message.from_user is not None
|
||||
|
||||
deeplink = command.args
|
||||
logger.info(
|
||||
"Deeplink start: user_id=%d, username=%s, deeplink=%s",
|
||||
message.from_user.id,
|
||||
message.from_user.username,
|
||||
deeplink,
|
||||
)
|
||||
|
||||
if not deeplink:
|
||||
await start_handler(message, user_dao, group_dao, dialog_manager)
|
||||
return
|
||||
@@ -122,6 +128,7 @@ async def start_with_deeplink(
|
||||
try:
|
||||
test_id = decode_id(deeplink, config.security.encode_key)
|
||||
except (ValueError, IndexError):
|
||||
logger.warning("Invalid deeplink: user_id=%d, deeplink=%s", message.from_user.id, deeplink)
|
||||
await message.answer("❌ Неверная ссылка на тест")
|
||||
await start_handler(message, user_dao, group_dao, dialog_manager)
|
||||
return
|
||||
@@ -138,6 +145,12 @@ async def start_with_deeplink(
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
logger.info(
|
||||
"Test validation failed: user_id=%d, test_id=%d, error=%s",
|
||||
message.from_user.id,
|
||||
test_id,
|
||||
error,
|
||||
)
|
||||
await dialog_manager.start(
|
||||
UserDeeplinkSG.test_preview,
|
||||
mode=StartMode.RESET_STACK,
|
||||
@@ -145,6 +158,7 @@ async def start_with_deeplink(
|
||||
)
|
||||
return
|
||||
|
||||
logger.info("User starting test via deeplink: user_id=%d, test_id=%d", message.from_user.id, test_id)
|
||||
await dialog_manager.start(
|
||||
UserDeeplinkSG.test_preview,
|
||||
mode=StartMode.RESET_STACK,
|
||||
@@ -159,6 +173,13 @@ async def start_handler(
|
||||
user_dao: FromDishka[UserDAO],
|
||||
group_dao: FromDishka[GroupDAO],
|
||||
) -> None:
|
||||
assert message.from_user is not None
|
||||
logger.info(
|
||||
"Start command: user_id=%d, username=%s",
|
||||
message.from_user.id,
|
||||
message.from_user.username,
|
||||
)
|
||||
|
||||
is_registered = await ensure_user_registered(
|
||||
user_dao, group_dao, message, dialog_manager
|
||||
)
|
||||
@@ -169,15 +190,22 @@ async def start_handler(
|
||||
|
||||
@router.message(Command("admin"))
|
||||
async def admin_command(_message: Message, dialog_manager: DialogManager) -> None:
|
||||
assert _message.from_user is not None
|
||||
logger.info("Admin panel access: user_id=%d", _message.from_user.id)
|
||||
await dialog_manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK)
|
||||
|
||||
|
||||
@router.message(Command("creator"))
|
||||
async def creator_command(_message: Message, dialog_manager: DialogManager) -> None:
|
||||
assert _message.from_user is not None
|
||||
logger.info("Creator panel access: user_id=%d", _message.from_user.id)
|
||||
await dialog_manager.start(CreatorMenuSG.main, mode=StartMode.RESET_STACK)
|
||||
|
||||
|
||||
@router.error()
|
||||
async def dialog_error_handler(event: ErrorEvent, dialog_manager: DialogManager) -> None:
|
||||
if isinstance(event.exception, (UnknownIntent, OutdatedIntent)):
|
||||
logger.debug("Dialog intent error, resetting to main menu: %s", type(event.exception).__name__)
|
||||
await dialog_manager.start(UserMenuSG.main, mode=StartMode.RESET_STACK)
|
||||
else:
|
||||
logger.exception("Unhandled error in dialog: %s", event.exception)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import functools
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
||||
@@ -21,7 +21,6 @@ from trudex.infrastructure.utils.config import Config
|
||||
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes
|
||||
from trudex.infrastructure.utils.test_id_to_hash import encode_id
|
||||
from trudex.infrastructure.utils.timezone import now_msk, now_msk_naive, to_msk
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def can_edit_field(updated_at: datetime | None) -> bool:
|
||||
|
||||
@@ -6,11 +6,7 @@ from aiogram_dialog.widgets.text import Const, Format
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.user_dialogs.states import (
|
||||
UserDeeplinkSG,
|
||||
UserMenuSG,
|
||||
UserRegistrationSG,
|
||||
)
|
||||
from trudex.application.bot.user_dialogs.states import UserDeeplinkSG, UserMenuSG, UserRegistrationSG
|
||||
from trudex.infrastructure.database.dao.group import GroupDAO
|
||||
from trudex.infrastructure.database.dao.user import UserDAO
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from trudex.application.bot.user_dialogs.states import UserMenuSG, UserTestSG
|
||||
from trudex.domain.schemas import QuestionType
|
||||
from trudex.infrastructure.database.dao.test import TestDAO
|
||||
from trudex.infrastructure.database.dao.user_answer import UserAnswerDAO
|
||||
from trudex.infrastructure.database.models import QuestionType
|
||||
from trudex.infrastructure.database.repo.test import TestRepository
|
||||
from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
||||
from trudex.infrastructure.utils.timezone import now_msk_naive
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class QuestionType(str, Enum):
|
||||
SINGLE = "single"
|
||||
MULTIPLE = "multiple"
|
||||
INPUT = "input"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -46,7 +53,7 @@ class Question:
|
||||
test_id: int
|
||||
text: str
|
||||
position: int = 0
|
||||
question_type: str = "single"
|
||||
question_type: QuestionType = QuestionType.SINGLE
|
||||
tg_file_id: str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker,
|
||||
create_async_engine)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
|
||||
def new_session_maker(db_url: str) -> async_sessionmaker[AsyncSession]:
|
||||
|
||||
@@ -2,8 +2,9 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from trudex.domain.schemas import Question as DomainQuestion
|
||||
from trudex.domain.schemas import QuestionType
|
||||
from trudex.infrastructure.database.dto.question import QuestionDTO
|
||||
from trudex.infrastructure.database.models import Question, QuestionType
|
||||
from trudex.infrastructure.database.models import Question
|
||||
|
||||
|
||||
class QuestionDAO:
|
||||
@@ -27,14 +28,16 @@ class QuestionDAO:
|
||||
test_id: int,
|
||||
text: str,
|
||||
position: int = 0,
|
||||
question_type: str = "single",
|
||||
question_type: str | QuestionType = QuestionType.SINGLE,
|
||||
tg_file_id: str | None = None,
|
||||
) -> DomainQuestion:
|
||||
if isinstance(question_type, str):
|
||||
question_type = QuestionType(question_type)
|
||||
question = Question(
|
||||
test_id=test_id,
|
||||
text=text,
|
||||
position=position,
|
||||
question_type=QuestionType(question_type),
|
||||
question_type=question_type,
|
||||
tg_file_id=tg_file_id,
|
||||
)
|
||||
self.session.add(question)
|
||||
@@ -47,7 +50,7 @@ class QuestionDAO:
|
||||
question_id: int,
|
||||
text: str | None = None,
|
||||
position: int | None = None,
|
||||
question_type: str | None = None,
|
||||
question_type: str | QuestionType | None = None,
|
||||
tg_file_id: str | None = None,
|
||||
) -> DomainQuestion | None:
|
||||
result = await self.session.execute(
|
||||
@@ -62,7 +65,9 @@ class QuestionDAO:
|
||||
if position is not None:
|
||||
question.position = position
|
||||
if question_type is not None:
|
||||
question.question_type = QuestionType(question_type)
|
||||
if isinstance(question_type, str):
|
||||
question_type = QuestionType(question_type)
|
||||
question.question_type = question_type
|
||||
if tg_file_id is not None:
|
||||
question.tg_file_id = tg_file_id
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import NotRequired, TypedDict, Unpack
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -8,6 +9,25 @@ from trudex.infrastructure.database.dto.test import TestDTO
|
||||
from trudex.infrastructure.database.models import Test
|
||||
|
||||
|
||||
class _UNSET:
|
||||
"""Sentinel для различения None и "не передано"."""
|
||||
pass
|
||||
|
||||
|
||||
UNSET = _UNSET()
|
||||
|
||||
|
||||
class TestUpdateFields(TypedDict, total=False):
|
||||
title: str
|
||||
description: str | None
|
||||
for_group: int | None
|
||||
password: str | None
|
||||
expires_at: datetime | None
|
||||
attempts: int | None
|
||||
is_active: bool
|
||||
are_results_viewable: bool
|
||||
|
||||
|
||||
class TestDAO:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self.session: AsyncSession = session
|
||||
@@ -26,6 +46,16 @@ class TestDAO:
|
||||
models = list(result.scalars().all())
|
||||
return [TestDTO(model).to_domain() for model in models]
|
||||
|
||||
async def get_expired_active_tests(self, now: datetime) -> list[DomainTest]:
|
||||
result = await self.session.execute(
|
||||
select(Test)
|
||||
.where(Test.is_active == True)
|
||||
.where(Test.expires_at.isnot(None))
|
||||
.where(Test.expires_at < now)
|
||||
)
|
||||
models = list(result.scalars().all())
|
||||
return [TestDTO(model).to_domain() for model in models]
|
||||
|
||||
async def create(
|
||||
self,
|
||||
title: str,
|
||||
@@ -55,14 +85,14 @@ class TestDAO:
|
||||
async def update(
|
||||
self,
|
||||
test_id: int,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
for_group: int | None = None,
|
||||
password: str | None = None,
|
||||
expires_at: datetime | None = None,
|
||||
attempts: int | None = None,
|
||||
is_active: bool | None = None,
|
||||
are_results_viewable: bool | None = None,
|
||||
title: str | _UNSET = UNSET,
|
||||
description: str | None | _UNSET = UNSET,
|
||||
for_group: int | None | _UNSET = UNSET,
|
||||
password: str | None | _UNSET = UNSET,
|
||||
expires_at: datetime | None | _UNSET = UNSET,
|
||||
attempts: int | None | _UNSET = UNSET,
|
||||
is_active: bool | _UNSET = UNSET,
|
||||
are_results_viewable: bool | _UNSET = UNSET,
|
||||
) -> DomainTest | None:
|
||||
result = await self.session.execute(
|
||||
select(Test).where(Test.id == test_id)
|
||||
@@ -71,21 +101,21 @@ class TestDAO:
|
||||
if not test:
|
||||
return None
|
||||
|
||||
if title is not None:
|
||||
if not isinstance(title, _UNSET):
|
||||
test.title = title
|
||||
if description is not None:
|
||||
if not isinstance(description, _UNSET):
|
||||
test.description = description
|
||||
if for_group is not None:
|
||||
if not isinstance(for_group, _UNSET):
|
||||
test.for_group = for_group
|
||||
if password is not None:
|
||||
if not isinstance(password, _UNSET):
|
||||
test.password = password
|
||||
if expires_at is not None:
|
||||
if not isinstance(expires_at, _UNSET):
|
||||
test.expires_at = expires_at
|
||||
if attempts is not None:
|
||||
if not isinstance(attempts, _UNSET):
|
||||
test.attempts = attempts
|
||||
if is_active is not None:
|
||||
if not isinstance(is_active, _UNSET):
|
||||
test.is_active = is_active
|
||||
if are_results_viewable is not None:
|
||||
if not isinstance(are_results_viewable, _UNSET):
|
||||
test.are_results_viewable = are_results_viewable
|
||||
|
||||
await self.session.flush()
|
||||
|
||||
@@ -8,6 +8,14 @@ from trudex.infrastructure.database.dto.user import UserDTO
|
||||
from trudex.infrastructure.database.models import User
|
||||
|
||||
|
||||
class _UNSET:
|
||||
"""Sentinel для различения None и "не передано"."""
|
||||
pass
|
||||
|
||||
|
||||
UNSET = _UNSET()
|
||||
|
||||
|
||||
class UserDAO:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self.session: AsyncSession = session
|
||||
@@ -53,14 +61,14 @@ class UserDAO:
|
||||
async def update(
|
||||
self,
|
||||
user_id: int,
|
||||
username: str | None = None,
|
||||
first_name: str | None = None,
|
||||
last_name: str | None = None,
|
||||
name: str | None = None,
|
||||
group: int | None = None,
|
||||
is_admin: bool | None = None,
|
||||
name_updated_at: datetime | None = None,
|
||||
group_updated_at: datetime | None = None,
|
||||
username: str | None | _UNSET = UNSET,
|
||||
first_name: str | _UNSET = UNSET,
|
||||
last_name: str | None | _UNSET = UNSET,
|
||||
name: str | None | _UNSET = UNSET,
|
||||
group: int | None | _UNSET = UNSET,
|
||||
is_admin: bool | _UNSET = UNSET,
|
||||
name_updated_at: datetime | None | _UNSET = UNSET,
|
||||
group_updated_at: datetime | None | _UNSET = UNSET,
|
||||
) -> DomainUser | None:
|
||||
result = await self.session.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
@@ -69,21 +77,21 @@ class UserDAO:
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if username is not None:
|
||||
if not isinstance(username, _UNSET):
|
||||
user.username = username
|
||||
if first_name is not None:
|
||||
if not isinstance(first_name, _UNSET):
|
||||
user.first_name = first_name
|
||||
if last_name is not None:
|
||||
if not isinstance(last_name, _UNSET):
|
||||
user.last_name = last_name
|
||||
if name is not None:
|
||||
if not isinstance(name, _UNSET):
|
||||
user.name = name
|
||||
if group is not None:
|
||||
if not isinstance(group, _UNSET):
|
||||
user.group = group
|
||||
if is_admin is not None:
|
||||
if not isinstance(is_admin, _UNSET):
|
||||
user.is_admin = is_admin
|
||||
if name_updated_at is not None:
|
||||
if not isinstance(name_updated_at, _UNSET):
|
||||
user.name_updated_at = name_updated_at
|
||||
if group_updated_at is not None:
|
||||
if not isinstance(group_updated_at, _UNSET):
|
||||
user.group_updated_at = group_updated_at
|
||||
|
||||
await self.session.flush()
|
||||
@@ -108,9 +116,9 @@ class UserDAO:
|
||||
first_name: str,
|
||||
username: str | None = None,
|
||||
last_name: str | None = None,
|
||||
name: str | None = None,
|
||||
group: int | None = None,
|
||||
is_admin: bool | None = None,
|
||||
name: str | None | _UNSET = UNSET,
|
||||
group: int | None | _UNSET = UNSET,
|
||||
is_admin: bool | _UNSET = UNSET,
|
||||
) -> DomainUser:
|
||||
result = await self.session.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
@@ -118,17 +126,14 @@ class UserDAO:
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user:
|
||||
if username is not None:
|
||||
user.username = username
|
||||
if first_name is not None:
|
||||
user.first_name = first_name
|
||||
if last_name is not None:
|
||||
user.last_name = last_name
|
||||
if name is not None:
|
||||
user.username = username
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
if not isinstance(name, _UNSET):
|
||||
user.name = name
|
||||
if group is not None:
|
||||
if not isinstance(group, _UNSET):
|
||||
user.group = group
|
||||
if is_admin is not None:
|
||||
if not isinstance(is_admin, _UNSET):
|
||||
user.is_admin = is_admin
|
||||
await self.session.flush()
|
||||
await self.session.refresh(user)
|
||||
@@ -139,7 +144,7 @@ class UserDAO:
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
name=name,
|
||||
group=group,
|
||||
is_admin=is_admin or False,
|
||||
name=name if not isinstance(name, _UNSET) else None,
|
||||
group=group if not isinstance(group, _UNSET) else None,
|
||||
is_admin=is_admin if not isinstance(is_admin, _UNSET) else False,
|
||||
)
|
||||
|
||||
@@ -12,6 +12,6 @@ class QuestionDTO:
|
||||
test_id=self.model.test_id,
|
||||
text=self.model.text,
|
||||
position=self.model.position,
|
||||
question_type=self.model.question_type.value,
|
||||
question_type=self.model.question_type,
|
||||
tg_file_id=self.model.tg_file_id,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from trudex.domain.schemas import TestAttempt as DomainTestAttempt
|
||||
from trudex.infrastructure.database.models import \
|
||||
TestAttempt as TestAttemptModel
|
||||
from trudex.infrastructure.database.models import TestAttempt as TestAttemptModel
|
||||
|
||||
|
||||
class TestAttemptDTO:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import final
|
||||
|
||||
from sqlalchemy import (BigInteger, CheckConstraint, ForeignKey, Integer,
|
||||
String, Text, func)
|
||||
from sqlalchemy import BigInteger, CheckConstraint, ForeignKey, Integer, String, Text, func
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
from trudex.domain.schemas import QuestionType
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
@@ -40,15 +40,6 @@ class Group(Base):
|
||||
__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):
|
||||
SINGLE = "single"
|
||||
MULTIPLE = "multiple"
|
||||
INPUT = "input"
|
||||
|
||||
|
||||
@final
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from trudex.infrastructure.database.repo.test import TestRepository
|
||||
from trudex.infrastructure.database.repo.test_attempt import \
|
||||
TestAttemptRepository
|
||||
from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
||||
from trudex.infrastructure.database.repo.user import UserRepository
|
||||
|
||||
__all__ = ["TestRepository", "TestAttemptRepository", "UserRepository"]
|
||||
|
||||
@@ -9,8 +9,7 @@ from trudex.infrastructure.database.dao.test_attempt import TestAttemptDAO
|
||||
from trudex.infrastructure.database.dao.user_answer import UserAnswerDAO
|
||||
from trudex.infrastructure.database.dto.test_attempt import TestAttemptDTO
|
||||
from trudex.infrastructure.database.dto.user_answer import UserAnswerDTO
|
||||
from trudex.infrastructure.database.models import \
|
||||
TestAttempt as TestAttemptModel
|
||||
from trudex.infrastructure.database.models import TestAttempt as TestAttemptModel
|
||||
from trudex.infrastructure.database.models import UserAnswer as UserAnswerModel
|
||||
from trudex.infrastructure.utils.timezone import now_msk_naive
|
||||
|
||||
@@ -176,8 +175,7 @@ class TestAttemptRepository:
|
||||
}
|
||||
|
||||
async def get_most_difficult_questions(self, test_id: int, limit: int = 10) -> list[tuple[int, float]]:
|
||||
from trudex.infrastructure.database.models import \
|
||||
Question as QuestionModel
|
||||
from trudex.infrastructure.database.models import Question as QuestionModel
|
||||
|
||||
result = await self.session.execute(
|
||||
select(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from collections.abc import AsyncIterable
|
||||
import logging
|
||||
from collections.abc import AsyncIterable
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from dishka import AsyncContainer, Provider, Scope, provide
|
||||
@@ -14,8 +14,7 @@ from trudex.infrastructure.database.dao.test_attempt import TestAttemptDAO
|
||||
from trudex.infrastructure.database.dao.user import UserDAO
|
||||
from trudex.infrastructure.database.dao.user_answer import UserAnswerDAO
|
||||
from trudex.infrastructure.database.repo.test import TestRepository
|
||||
from trudex.infrastructure.database.repo.test_attempt import \
|
||||
TestAttemptRepository
|
||||
from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
||||
from trudex.infrastructure.database.repo.user import UserRepository
|
||||
from trudex.infrastructure.scheduling.tasks import deactivate_expired_tests
|
||||
from trudex.infrastructure.utils.config import Config
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import logging
|
||||
|
||||
from dishka import AsyncContainer
|
||||
|
||||
from trudex.infrastructure.database.dao.test import TestDAO
|
||||
from trudex.infrastructure.utils.timezone import now_msk_naive
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def deactivate_expired_tests(container: AsyncContainer):
|
||||
|
||||
async def deactivate_expired_tests(container: AsyncContainer) -> None:
|
||||
async with container() as request_container:
|
||||
test_dao = await request_container.get(TestDAO)
|
||||
|
||||
tests = await test_dao.get_all()
|
||||
expired_tests = await test_dao.get_expired_active_tests(now_msk_naive())
|
||||
|
||||
for test in tests:
|
||||
if test.expires_at and test.expires_at < now_msk_naive() and test.is_active:
|
||||
await test_dao.update(test.id, is_active=False)
|
||||
for test in expired_tests:
|
||||
await test_dao.update(test.id, is_active=False)
|
||||
logger.info("Деактивирован истёкший тест: id=%d, title=%s", test.id, test.title)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from aiogram import Bot
|
||||
from aiogram.types import (BotCommand, BotCommandScopeAllPrivateChats,
|
||||
BotCommandScopeChat)
|
||||
from aiogram.types import BotCommand, BotCommandScopeAllPrivateChats, BotCommandScopeChat
|
||||
|
||||
from trudex.infrastructure.database.repo.user import UserRepository
|
||||
from trudex.infrastructure.utils.config import Config
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiogram import Bot
|
||||
@@ -6,6 +7,8 @@ from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
|
||||
from trudex.infrastructure.database.dao.user import UserDAO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BroadcastStats:
|
||||
@@ -19,17 +22,20 @@ async def broadcast_message(bot: Bot, message_id: int, chat_id: int, user_dao: U
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
logger.info("Starting broadcast: message_id=%d, total_users=%d", message_id, len(users))
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
await bot.copy_message(chat_id=user.id, from_chat_id=chat_id, message_id=message_id)
|
||||
success += 1
|
||||
except TelegramForbiddenError:
|
||||
logger.debug("Broadcast failed (forbidden): user_id=%d", user.id)
|
||||
failed += 1
|
||||
except TelegramBadRequest:
|
||||
failed += 1
|
||||
except Exception:
|
||||
except TelegramBadRequest as e:
|
||||
logger.debug("Broadcast failed (bad request): user_id=%d, error=%s", user.id, e)
|
||||
failed += 1
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
logger.info("Broadcast completed: success=%d, failed=%d, total=%d", success, failed, len(users))
|
||||
return BroadcastStats(success=success, failed=failed, total=len(users))
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import hmac
|
||||
import hashlib
|
||||
import hmac
|
||||
import string
|
||||
|
||||
|
||||
ALPHABET = string.ascii_letters
|
||||
|
||||
def _feistel_round(val: int, key: bytes, rounds: int) -> int:
|
||||
|
||||
Reference in New Issue
Block a user