From 02b6ad48bb25ca80bb4860644f90628b09a98cdb Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 3 Jan 2026 23:29:03 +0300 Subject: [PATCH] commit --- .../application/bot/admin_dialogs/states.py | 2 + .../application/bot/admin_dialogs/tests.py | 116 ++++++++++++++++- .../application/bot/creator_dialogs/states.py | 2 + .../application/bot/creator_dialogs/tests.py | 118 +++++++++++++++++- .../bot/user_dialogs/registration.py | 3 - .../database/repo/test_attempt.py | 13 ++ 6 files changed, 246 insertions(+), 8 deletions(-) diff --git a/src/trudex/application/bot/admin_dialogs/states.py b/src/trudex/application/bot/admin_dialogs/states.py index b55d1ae..44f013e 100644 --- a/src/trudex/application/bot/admin_dialogs/states.py +++ b/src/trudex/application/bot/admin_dialogs/states.py @@ -20,6 +20,8 @@ class AdminTestsSG(StatesGroup): edit_attempts = State() edit_group = State() edit_expires = State() + statistics = State() + attempt_detail = State() class AdminBroadcastSG(StatesGroup): diff --git a/src/trudex/application/bot/admin_dialogs/tests.py b/src/trudex/application/bot/admin_dialogs/tests.py index 2c07051..0bf85e0 100644 --- a/src/trudex/application/bot/admin_dialogs/tests.py +++ b/src/trudex/application/bot/admin_dialogs/tests.py @@ -17,6 +17,7 @@ from trudex.application.bot.admin_dialogs.states import (AdminMenuSG, from trudex.infrastructure.database.dao.group import GroupDAO from trudex.infrastructure.database.dao.test import TestDAO from trudex.infrastructure.database.repo.test import TestRepository +from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository 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 @@ -109,8 +110,92 @@ async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: Di await manager.switch_to(AdminTestsSG.tests_list) -async def on_statistics(_callback: CallbackQuery, _button: Button, _manager: DialogManager): - await _callback.answer("🚧 В разработке") +async def on_statistics(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.switch_to(AdminTestsSG.statistics) + + +@inject +async def get_statistics_data( + dialog_manager: DialogManager, + attempt_repo: FromDishka[TestAttemptRepository], + **_kwargs +): + test_id = dialog_manager.dialog_data.get("selected_test_id") + + if not test_id: + return {"attempts": [], "count": 0} + + attempts_with_users = await attempt_repo.get_test_attempts_with_users(test_id) + + results = [] + for attempt, user_name in attempts_with_users: + status = "✅" if attempt.is_passed else "❌" + date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else "" + results.append((f"{status} {user_name} — {attempt.score}% ({date_str})", attempt.id)) + + return { + "attempts": results, + "count": len(results), + } + + +async def on_attempt_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str): + manager.dialog_data["selected_attempt_id"] = int(item_id) + await manager.switch_to(AdminTestsSG.attempt_detail) + + +async def on_back_to_statistics(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.switch_to(AdminTestsSG.statistics) + + +@inject +async def get_attempt_detail( + dialog_manager: DialogManager, + attempt_repo: FromDishka[TestAttemptRepository], + test_repo: FromDishka[TestRepository], + **_kwargs +): + attempt_id = dialog_manager.dialog_data.get("selected_attempt_id") + + if not attempt_id: + return {"attempt_info": "❌ Результат не найден"} + + attempt, answers = await attempt_repo.get_attempt_with_answers(attempt_id) + + if not attempt: + return {"attempt_info": "❌ Результат не найден"} + + status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден" + date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else "—" + + lines = [ + f"📊 Результат прохождения\n", + f"📈 Результат: {attempt.score}%", + f"📅 Дата: {date_str}", + f"🏆 Статус: {status}\n", + "📋 Ответы:\n", + ] + + for i, answer in enumerate(answers, 1): + question, options = await test_repo.get_question_with_options(answer.question_id) + if not question: + continue + + correct_options = [opt for opt in options if opt.is_correct] + correct_texts = [opt.text for opt in correct_options] + + status_icon = "✅" if answer.is_correct else "❌" + + user_answer = answer.text_answer or "" + if "|" in user_answer: + user_answer = ", ".join(user_answer.split("|")) + + lines.append(f"{status_icon} Вопрос {i}") + lines.append(f"
{question.text}
") + lines.append(f"👤 Ответ: {user_answer or '—'}") + lines.append(f"✓ Правильно: {', '.join(correct_texts)}\n") + + return {"attempt_info": "\n".join(lines)} @inject @@ -413,4 +498,31 @@ tests_dialog = Dialog( ), state=AdminTestsSG.edit_expires, ), + Window( + Format("📊 Статистика теста\n\nПрошли тест: {count}"), + ScrollingGroup( + Select( + Format("{item[0]}"), + id="attempt_select", + item_id_getter=lambda x: x[1], + items="attempts", + on_click=on_attempt_selected, + ), + id="attempts_scroll", + width=1, + height=7, + ), + Column( + Button(Const("🔄 Обновить"), id="refresh", on_click=on_statistics), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail), + ), + state=AdminTestsSG.statistics, + getter=get_statistics_data, + ), + Window( + Format("{attempt_info}"), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_statistics), + state=AdminTestsSG.attempt_detail, + getter=get_attempt_detail, + ), ) diff --git a/src/trudex/application/bot/creator_dialogs/states.py b/src/trudex/application/bot/creator_dialogs/states.py index 2d5fbe0..bc74887 100644 --- a/src/trudex/application/bot/creator_dialogs/states.py +++ b/src/trudex/application/bot/creator_dialogs/states.py @@ -21,6 +21,8 @@ class CreatorTestsSG(StatesGroup): edit_attempts = State() edit_group = State() edit_expires = State() + statistics = State() + attempt_detail = State() class CreatorBroadcastSG(StatesGroup): diff --git a/src/trudex/application/bot/creator_dialogs/tests.py b/src/trudex/application/bot/creator_dialogs/tests.py index 914fecc..e690362 100644 --- a/src/trudex/application/bot/creator_dialogs/tests.py +++ b/src/trudex/application/bot/creator_dialogs/tests.py @@ -22,6 +22,7 @@ from trudex.application.bot.creator_dialogs.states import (CreateTestSG, from trudex.infrastructure.database.dao.group import GroupDAO from trudex.infrastructure.database.dao.test import TestDAO from trudex.infrastructure.database.repo.test import TestRepository +from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository 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 @@ -114,8 +115,92 @@ async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: Di await manager.switch_to(CreatorTestsSG.tests_list) -async def on_statistics(_callback: CallbackQuery, _button: Button, _manager: DialogManager): - await _callback.answer("🚧 В разработке") +async def on_statistics(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.switch_to(CreatorTestsSG.statistics) + + +@inject +async def get_statistics_data( + dialog_manager: DialogManager, + attempt_repo: FromDishka[TestAttemptRepository], + **_kwargs +): + test_id = dialog_manager.dialog_data.get("selected_test_id") + + if not test_id: + return {"attempts": [], "count": 0} + + attempts_with_users = await attempt_repo.get_test_attempts_with_users(test_id) + + results = [] + for attempt, user_name in attempts_with_users: + status = "✅" if attempt.is_passed else "❌" + date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else "" + results.append((f"{status} {user_name} — {attempt.score}% ({date_str})", attempt.id)) + + return { + "attempts": results, + "count": len(results), + } + + +async def on_attempt_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str): + manager.dialog_data["selected_attempt_id"] = int(item_id) + await manager.switch_to(CreatorTestsSG.attempt_detail) + + +async def on_back_to_statistics(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.switch_to(CreatorTestsSG.statistics) + + +@inject +async def get_attempt_detail( + dialog_manager: DialogManager, + attempt_repo: FromDishka[TestAttemptRepository], + test_repo: FromDishka[TestRepository], + **_kwargs +): + attempt_id = dialog_manager.dialog_data.get("selected_attempt_id") + + if not attempt_id: + return {"attempt_info": "❌ Результат не найден"} + + attempt, answers = await attempt_repo.get_attempt_with_answers(attempt_id) + + if not attempt: + return {"attempt_info": "❌ Результат не найден"} + + status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден" + date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else "—" + + lines = [ + f"📊 Результат прохождения\n", + f"📈 Результат: {attempt.score}%", + f"📅 Дата: {date_str}", + f"🏆 Статус: {status}\n", + "📋 Ответы:\n", + ] + + for i, answer in enumerate(answers, 1): + question, options = await test_repo.get_question_with_options(answer.question_id) + if not question: + continue + + correct_options = [opt for opt in options if opt.is_correct] + correct_texts = [opt.text for opt in correct_options] + + status_icon = "✅" if answer.is_correct else "❌" + + user_answer = answer.text_answer or "" + if "|" in user_answer: + user_answer = ", ".join(user_answer.split("|")) + + lines.append(f"{status_icon} Вопрос {i}") + lines.append(f"
{question.text}
") + lines.append(f"👤 Ответ: {user_answer or '—'}") + lines.append(f"✓ Правильно: {', '.join(correct_texts)}\n") + + return {"attempt_info": "\n".join(lines)} @inject @@ -418,6 +503,33 @@ tests_dialog = Dialog( Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu), ), state=CreatorTestsSG.edit_expires, - ) + ), + Window( + Format("📊 Статистика теста\n\nПрошли тест: {count}"), + ScrollingGroup( + Select( + Format("{item[0]}"), + id="attempt_select", + item_id_getter=lambda x: x[1], + items="attempts", + on_click=on_attempt_selected, + ), + id="attempts_scroll", + width=1, + height=7, + ), + Column( + Button(Const("🔄 Обновить"), id="refresh", on_click=on_statistics), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail), + ), + state=CreatorTestsSG.statistics, + getter=get_statistics_data, + ), + Window( + Format("{attempt_info}"), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_statistics), + state=CreatorTestsSG.attempt_detail, + getter=get_attempt_detail, + ), ) diff --git a/src/trudex/application/bot/user_dialogs/registration.py b/src/trudex/application/bot/user_dialogs/registration.py index 632e983..b7ba040 100644 --- a/src/trudex/application/bot/user_dialogs/registration.py +++ b/src/trudex/application/bot/user_dialogs/registration.py @@ -46,10 +46,7 @@ async def get_groups_for_registration(dialog_manager: DialogManager, group_dao: @inject async def on_group_selected(_callback: CallbackQuery, _widget, manager: DialogManager, item_id: str, user_dao: FromDishka[UserDAO]): user_id = manager.start_data.get("user_id") - await user_dao.update(user_id=user_id, group=int(item_id)) - - await _callback.answer("✅ Группа выбрана! Вы можете изменить её через 24 часа", show_alert=True) await manager.start(UserMenuSG.main, mode=StartMode.RESET_STACK) diff --git a/src/trudex/infrastructure/database/repo/test_attempt.py b/src/trudex/infrastructure/database/repo/test_attempt.py index 2f8be01..9a822c2 100644 --- a/src/trudex/infrastructure/database/repo/test_attempt.py +++ b/src/trudex/infrastructure/database/repo/test_attempt.py @@ -224,3 +224,16 @@ class TestAttemptRepository: ) rows = result.all() return [(TestAttemptDTO(row[0]).to_domain(), row[1]) for row in rows] + + async def get_test_attempts_with_users(self, test_id: int) -> list[tuple[TestAttempt, str]]: + from trudex.infrastructure.database.models import User as UserModel + + result = await self.session.execute( + select(TestAttemptModel, UserModel.name, UserModel.first_name) + .join(UserModel, TestAttemptModel.user_id == UserModel.id) + .where(TestAttemptModel.test_id == test_id) + .where(TestAttemptModel.finished_at.isnot(None)) + .order_by(TestAttemptModel.finished_at.desc()) + ) + rows = result.all() + return [(TestAttemptDTO(row[0]).to_domain(), row[1] or row[2]) for row in rows]