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