From 15e3815f7171e9bb19ec1ad6e39f5b00b1ff9fa6 Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 3 Jan 2026 23:04:34 +0300 Subject: [PATCH] commit --- .../application/bot/user_dialogs/main_menu.py | 111 +++++++++++++++++- .../application/bot/user_dialogs/states.py | 2 + .../database/repo/test_attempt.py | 13 ++ 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/src/trudex/application/bot/user_dialogs/main_menu.py b/src/trudex/application/bot/user_dialogs/main_menu.py index de777ff..e5530bc 100644 --- a/src/trudex/application/bot/user_dialogs/main_menu.py +++ b/src/trudex/application/bot/user_dialogs/main_menu.py @@ -107,8 +107,8 @@ async def on_tests_clicked(_callback: CallbackQuery, _button: Button, manager: D await manager.switch_to(UserMenuSG.available_tests) -async def on_results_clicked(_callback: CallbackQuery, _button: Button, _manager: DialogManager): - await _callback.answer("🚧 В разработке") +async def on_results_clicked(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.switch_to(UserMenuSG.my_results) async def on_back_to_main(_callback: CallbackQuery, _button: Button, manager: DialogManager): @@ -222,6 +222,89 @@ async def get_test_detail( return {"test_info": test_info} +@inject +async def get_my_results( + dialog_manager: DialogManager, + attempt_repo: FromDishka[TestAttemptRepository], + **_kwargs +): + user_id = dialog_manager.event.from_user.id + attempts_with_tests = await attempt_repo.get_finished_attempts_with_tests(user_id) + + results = [] + for attempt, test_title in attempts_with_tests: + status = "✅" if attempt.is_passed else "❌" + date_str = attempt.finished_at.strftime("%d.%m.%Y") if attempt.finished_at else "" + results.append((f"{status} {test_title} — {attempt.score}% ({date_str})", attempt.id)) + + return { + "results": results, + "count": len(results), + } + + +async def on_result_selected(_callback: CallbackQuery, _widget: Select, manager: DialogManager, item_id: str): + manager.dialog_data["selected_attempt_id"] = int(item_id) + await manager.switch_to(UserMenuSG.result_detail) + + +async def on_back_to_results(_callback: CallbackQuery, _button: Button, manager: DialogManager): + await manager.switch_to(UserMenuSG.my_results) + + +@inject +async def get_result_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 {"result_info": "❌ Результат не найден"} + + attempt, answers = await attempt_repo.get_attempt_with_answers(attempt_id) + + if not attempt: + return {"result_info": "❌ Результат не найден"} + + test, _ = await test_repo.get_test_with_questions(attempt.test_id) + test_title = test.title if test else "Неизвестный тест" + + status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден" + date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else "—" + + lines = [ + f"📝 {test_title}\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 {"result_info": "\n".join(lines)} + + user_menu_dialog = Dialog( Window( Format("{user_info}"), @@ -287,4 +370,28 @@ user_menu_dialog = Dialog( state=UserMenuSG.edit_group, getter=get_groups_data, ), + Window( + Format("📊 Мои результаты\n\nВсего: {count}"), + ScrollingGroup( + Select( + Format("{item[0]}"), + id="result_select", + item_id_getter=lambda x: x[1], + items="results", + on_click=on_result_selected, + ), + id="results_scroll", + width=1, + height=7, + ), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_main), + state=UserMenuSG.my_results, + getter=get_my_results, + ), + Window( + Format("{result_info}"), + Button(Const("◀️ Назад"), id="back", on_click=on_back_to_results), + state=UserMenuSG.result_detail, + getter=get_result_detail, + ), ) diff --git a/src/trudex/application/bot/user_dialogs/states.py b/src/trudex/application/bot/user_dialogs/states.py index b28c737..e485906 100644 --- a/src/trudex/application/bot/user_dialogs/states.py +++ b/src/trudex/application/bot/user_dialogs/states.py @@ -7,6 +7,8 @@ class UserMenuSG(StatesGroup): test_detail = State() edit_name = State() edit_group = State() + my_results = State() + result_detail = State() class UserTestSG(StatesGroup): diff --git a/src/trudex/infrastructure/database/repo/test_attempt.py b/src/trudex/infrastructure/database/repo/test_attempt.py index 1938f0c..2f8be01 100644 --- a/src/trudex/infrastructure/database/repo/test_attempt.py +++ b/src/trudex/infrastructure/database/repo/test_attempt.py @@ -211,3 +211,16 @@ class TestAttemptRepository: "total_attempts": row.total_attempts or 0, "avg_score": round(row.avg_score, 1) if row.avg_score else 0, } + + async def get_finished_attempts_with_tests(self, user_id: int) -> list[tuple[TestAttempt, str]]: + from trudex.infrastructure.database.models import Test as TestModel + + result = await self.session.execute( + select(TestAttemptModel, TestModel.title) + .join(TestModel, TestAttemptModel.test_id == TestModel.id) + .where(TestAttemptModel.user_id == user_id) + .where(TestAttemptModel.finished_at.isnot(None)) + .order_by(TestAttemptModel.finished_at.desc()) + ) + rows = result.all() + return [(TestAttemptDTO(row[0]).to_domain(), row[1]) for row in rows]