mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 18:35:28 +03:00
commit
This commit is contained in:
@@ -52,6 +52,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
||||
"test_info": "Тест не найден",
|
||||
"is_active": False,
|
||||
"button_text": "◀️ Назад",
|
||||
"results_button_text": "👁 Показать результаты",
|
||||
}
|
||||
|
||||
test = await test_dao.get_by_id(test_id)
|
||||
@@ -62,6 +63,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
||||
"test_info": "Тест не найден",
|
||||
"is_active": False,
|
||||
"button_text": "◀️ Назад",
|
||||
"results_button_text": "👁 Показать результаты",
|
||||
}
|
||||
|
||||
status = "🟢 Активен" if test.is_active else "🔴 Деактивирован"
|
||||
@@ -69,6 +71,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
||||
attempts_str = f"🔄 {test.attempts}" if test.attempts else "♾️ Без ограничений"
|
||||
expires_str = f"📅 {to_msk(test.expires_at).strftime('%d.%m.%Y %H:%M')}" if test.expires_at else "📅 Без срока"
|
||||
group_str = f"🎓 Группа {test.for_group}" if test.for_group else "👥 Для всех"
|
||||
results_str = "👁 Результаты видны" if test.are_results_viewable else "🔒 Результаты скрыты"
|
||||
|
||||
test_info = (
|
||||
f"<b>📝 Информация о тесте</b>\n\n"
|
||||
@@ -79,16 +82,19 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
||||
f"<b>Пароль:</b> {password_str}\n"
|
||||
f"<b>Попытки:</b> {attempts_str}\n"
|
||||
f"<b>Срок:</b> {expires_str}\n"
|
||||
f"<b>Группа:</b> {group_str}\n\n"
|
||||
f"<b>Группа:</b> {group_str}\n"
|
||||
f"<b>Видимость:</b> {results_str}\n\n"
|
||||
f"<b>Создан:</b> {to_msk(test.created_at).strftime('%d.%m.%Y %H:%M') if test.created_at else '—'}"
|
||||
)
|
||||
|
||||
button_text = "🔴 Деактивировать" if test.is_active else "🟢 Активировать"
|
||||
results_button_text = "🔒 Скрыть результаты" if test.are_results_viewable else "👁 Показать результаты"
|
||||
|
||||
return {
|
||||
"test_info": test_info,
|
||||
"is_active": test.is_active,
|
||||
"button_text": button_text,
|
||||
"results_button_text": results_button_text,
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +114,22 @@ async def on_toggle_active(_callback: CallbackQuery, _button: Button, manager: D
|
||||
await manager.switch_to(AdminTestsSG.test_detail)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_toggle_results_viewable(_callback: CallbackQuery, _button: Button, manager: DialogManager, test_dao: FromDishka[TestDAO]):
|
||||
test_id = manager.dialog_data.get("selected_test_id")
|
||||
if not test_id:
|
||||
await _callback.answer("❌ Тест не найден")
|
||||
return
|
||||
|
||||
test = await test_dao.get_by_id(test_id)
|
||||
|
||||
if test:
|
||||
await test_dao.update(test_id, are_results_viewable=not test.are_results_viewable)
|
||||
action = "скрыты" if test.are_results_viewable else "видны"
|
||||
await _callback.answer(f"✅ Результаты теперь {action}")
|
||||
await manager.switch_to(AdminTestsSG.test_detail)
|
||||
|
||||
|
||||
async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: DialogManager):
|
||||
await manager.switch_to(AdminTestsSG.tests_list)
|
||||
|
||||
@@ -435,6 +457,11 @@ tests_dialog = Dialog(
|
||||
id="toggle_active",
|
||||
on_click=on_toggle_active
|
||||
),
|
||||
Button(
|
||||
Format("{results_button_text}"),
|
||||
id="toggle_results",
|
||||
on_click=on_toggle_results_viewable
|
||||
),
|
||||
Button(Const("📊 Статистика"), id="statistics", on_click=on_statistics),
|
||||
Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test),
|
||||
Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu),
|
||||
|
||||
@@ -55,6 +55,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
||||
"test_info": "Тест не найден",
|
||||
"is_active": False,
|
||||
"button_text": "◀️ Назад",
|
||||
"results_button_text": "👁 Показать результаты",
|
||||
}
|
||||
|
||||
test = await test_dao.get_by_id(test_id)
|
||||
@@ -65,6 +66,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
||||
"test_info": "Тест не найден",
|
||||
"is_active": False,
|
||||
"button_text": "◀️ Назад",
|
||||
"results_button_text": "👁 Показать результаты",
|
||||
}
|
||||
|
||||
status = "🟢 Активен" if test.is_active else "🔴 Деактивирован"
|
||||
@@ -72,6 +74,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
||||
attempts_str = f"🔄 {test.attempts}" if test.attempts else "♾️ Без ограничений"
|
||||
expires_str = f"📅 {to_msk(test.expires_at).strftime('%d.%m.%Y %H:%M')}" if test.expires_at else "📅 Без срока"
|
||||
group_str = f"🎓 Группа {test.for_group}" if test.for_group else "👥 Для всех"
|
||||
results_str = "👁 Результаты видны" if test.are_results_viewable else "🔒 Результаты скрыты"
|
||||
|
||||
test_info = (
|
||||
f"<b>📝 Информация о тесте</b>\n\n"
|
||||
@@ -82,16 +85,19 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
||||
f"<b>Пароль:</b> {password_str}\n"
|
||||
f"<b>Попытки:</b> {attempts_str}\n"
|
||||
f"<b>Срок:</b> {expires_str}\n"
|
||||
f"<b>Группа:</b> {group_str}\n\n"
|
||||
f"<b>Группа:</b> {group_str}\n"
|
||||
f"<b>Видимость:</b> {results_str}\n\n"
|
||||
f"<b>Создан:</b> {to_msk(test.created_at).strftime('%d.%m.%Y %H:%M') if test.created_at else '—'}"
|
||||
)
|
||||
|
||||
button_text = "🔴 Деактивировать" if test.is_active else "🟢 Активировать"
|
||||
results_button_text = "🔒 Скрыть результаты" if test.are_results_viewable else "👁 Показать результаты"
|
||||
|
||||
return {
|
||||
"test_info": test_info,
|
||||
"is_active": test.is_active,
|
||||
"button_text": button_text,
|
||||
"results_button_text": results_button_text,
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +117,22 @@ async def on_toggle_active(_callback: CallbackQuery, _button: Button, manager: D
|
||||
await manager.switch_to(CreatorTestsSG.test_detail)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_toggle_results_viewable(_callback: CallbackQuery, _button: Button, manager: DialogManager, test_dao: FromDishka[TestDAO]):
|
||||
test_id = manager.dialog_data.get("selected_test_id")
|
||||
if not test_id:
|
||||
await _callback.answer("❌ Тест не найден")
|
||||
return
|
||||
|
||||
test = await test_dao.get_by_id(test_id)
|
||||
|
||||
if test:
|
||||
await test_dao.update(test_id, are_results_viewable=not test.are_results_viewable)
|
||||
action = "скрыты" if test.are_results_viewable else "видны"
|
||||
await _callback.answer(f"✅ Результаты теперь {action}")
|
||||
await manager.switch_to(CreatorTestsSG.test_detail)
|
||||
|
||||
|
||||
async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: DialogManager):
|
||||
await manager.switch_to(CreatorTestsSG.tests_list)
|
||||
|
||||
@@ -439,6 +461,11 @@ tests_dialog = Dialog(
|
||||
id="toggle_active",
|
||||
on_click=on_toggle_active
|
||||
),
|
||||
Button(
|
||||
Format("{results_button_text}"),
|
||||
id="toggle_results",
|
||||
on_click=on_toggle_results_viewable
|
||||
),
|
||||
Button(Const("📊 Статистика"), id="statistics", on_click=on_statistics),
|
||||
Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test),
|
||||
Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu),
|
||||
|
||||
@@ -348,37 +348,46 @@ async def get_result_detail(
|
||||
|
||||
test, _ = await test_repo.get_test_with_questions(attempt.test_id)
|
||||
test_title = test.title if test else "Неизвестный тест"
|
||||
are_results_viewable = test.are_results_viewable if test else False
|
||||
|
||||
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
||||
finished_at_msk = to_msk(attempt.finished_at)
|
||||
date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else "—"
|
||||
|
||||
correct_count = sum(1 for a in answers if a.is_correct)
|
||||
total_count = len(answers)
|
||||
|
||||
lines = [
|
||||
f"<b>📝 {test_title}</b>\n",
|
||||
f"📊 <b>Результат:</b> {attempt.score}%",
|
||||
f"✏️ <b>Правильных ответов:</b> {correct_count} из {total_count}",
|
||||
f"📅 <b>Дата:</b> {date_str}",
|
||||
f"🏆 <b>Статус:</b> {status}\n",
|
||||
"<b>📋 Ответы:</b>\n",
|
||||
f"🏆 <b>Статус:</b> {status}",
|
||||
]
|
||||
|
||||
for i, answer in enumerate(answers, 1):
|
||||
question, options = await test_repo.get_question_with_options(answer.question_id)
|
||||
if not question:
|
||||
continue
|
||||
if are_results_viewable:
|
||||
lines.append("\n<b>📋 Ответы:</b>\n")
|
||||
|
||||
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} <b>Вопрос {i}</b>")
|
||||
lines.append(f"<blockquote>{question.text}</blockquote>")
|
||||
lines.append(f"👤 <i>Ваш ответ:</i> {user_answer or '—'}")
|
||||
lines.append(f"✓ <i>Правильно:</i> {', '.join(correct_texts)}\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} <b>Вопрос {i}</b>")
|
||||
lines.append(f"<blockquote>{question.text}</blockquote>")
|
||||
lines.append(f"👤 <i>Ваш ответ:</i> {user_answer or '—'}")
|
||||
lines.append(f"✓ <i>Правильно:</i> {', '.join(correct_texts)}\n")
|
||||
else:
|
||||
lines.append("\n<i>🔒 Подробные результаты скрыты</i>")
|
||||
|
||||
return {"result_info": "\n".join(lines)}
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ async def on_text_answer_input(
|
||||
test_repo: FromDishka[TestRepository],
|
||||
attempt_repo: FromDishka[TestAttemptRepository],
|
||||
answer_dao: FromDishka[UserAnswerDAO],
|
||||
test_dao: FromDishka[TestDAO],
|
||||
):
|
||||
start_data = manager.start_data or {}
|
||||
assert isinstance(start_data, dict)
|
||||
@@ -248,6 +249,7 @@ async def on_text_answer_input(
|
||||
question_id = questions[current_index]
|
||||
text_answer = message.text.strip() if message.text else ""
|
||||
attempt_id = manager.dialog_data.get("attempt_id") or start_data.get("attempt_id")
|
||||
test_id = manager.dialog_data.get("test_id") or start_data.get("test_id")
|
||||
|
||||
if not attempt_id:
|
||||
await message.answer("❌ Ошибка попытки")
|
||||
@@ -270,7 +272,9 @@ async def on_text_answer_input(
|
||||
)
|
||||
|
||||
if current_index + 1 >= len(questions):
|
||||
await finish_test(manager, attempt_repo, attempt_id, len(questions))
|
||||
test = await test_dao.get_by_id(test_id) if test_id else None
|
||||
are_results_viewable = test.are_results_viewable if test else False
|
||||
await finish_test(manager, attempt_repo, attempt_id, len(questions), are_results_viewable)
|
||||
else:
|
||||
next_index = current_index + 1
|
||||
manager.dialog_data["current_question_index"] = next_index
|
||||
@@ -290,6 +294,7 @@ async def on_next_question(
|
||||
test_repo: FromDishka[TestRepository],
|
||||
attempt_repo: FromDishka[TestAttemptRepository],
|
||||
answer_dao: FromDishka[UserAnswerDAO],
|
||||
test_dao: FromDishka[TestDAO],
|
||||
):
|
||||
start_data = manager.start_data or {}
|
||||
assert isinstance(start_data, dict)
|
||||
@@ -311,6 +316,7 @@ async def on_next_question(
|
||||
|
||||
answer_data = user_answers[str(question_id)]
|
||||
attempt_id = manager.dialog_data.get("attempt_id") or start_data.get("attempt_id")
|
||||
test_id = manager.dialog_data.get("test_id") or start_data.get("test_id")
|
||||
|
||||
if not attempt_id:
|
||||
await _callback.answer("❌ Ошибка попытки")
|
||||
@@ -352,7 +358,9 @@ async def on_next_question(
|
||||
)
|
||||
|
||||
if current_index + 1 >= len(questions):
|
||||
await finish_test(manager, attempt_repo, attempt_id, len(questions))
|
||||
test = await test_dao.get_by_id(test_id) if test_id else None
|
||||
are_results_viewable = test.are_results_viewable if test else False
|
||||
await finish_test(manager, attempt_repo, attempt_id, len(questions), are_results_viewable)
|
||||
else:
|
||||
next_index = current_index + 1
|
||||
manager.dialog_data["current_question_index"] = next_index
|
||||
@@ -364,7 +372,13 @@ async def on_next_question(
|
||||
await manager.switch_to(next_state)
|
||||
|
||||
|
||||
async def finish_test(manager: DialogManager, attempt_repo: TestAttemptRepository, attempt_id: int, total_questions: int):
|
||||
async def finish_test(
|
||||
manager: DialogManager,
|
||||
attempt_repo: TestAttemptRepository,
|
||||
attempt_id: int,
|
||||
total_questions: int,
|
||||
are_results_viewable: bool = False,
|
||||
):
|
||||
correct_count = await attempt_repo.calculate_attempt_score(attempt_id)
|
||||
|
||||
score = round((correct_count / total_questions * 100)) if total_questions > 0 else 0
|
||||
@@ -376,6 +390,7 @@ async def finish_test(manager: DialogManager, attempt_repo: TestAttemptRepositor
|
||||
manager.dialog_data["correct_count"] = correct_count
|
||||
manager.dialog_data["total_questions"] = total_questions
|
||||
manager.dialog_data["is_passed"] = is_passed
|
||||
manager.dialog_data["are_results_viewable"] = are_results_viewable
|
||||
|
||||
await manager.switch_to(UserTestSG.results)
|
||||
|
||||
@@ -385,6 +400,7 @@ async def get_results_data(dialog_manager: DialogManager, **_kwargs):
|
||||
correct_count = dialog_manager.dialog_data.get("correct_count", 0)
|
||||
total_questions = dialog_manager.dialog_data.get("total_questions", 0)
|
||||
is_passed = dialog_manager.dialog_data.get("is_passed", False)
|
||||
are_results_viewable = dialog_manager.dialog_data.get("are_results_viewable", False)
|
||||
|
||||
if is_passed:
|
||||
status = "✅ <b>Тест пройден!</b>"
|
||||
@@ -397,7 +413,7 @@ async def get_results_data(dialog_manager: DialogManager, **_kwargs):
|
||||
f"✏️ <b>Правильных ответов:</b> {correct_count} из {total_questions}"
|
||||
)
|
||||
|
||||
return {"results_text": results_text}
|
||||
return {"results_text": results_text, "are_results_viewable": are_results_viewable}
|
||||
|
||||
|
||||
async def on_back_to_menu(_callback: CallbackQuery, _button: Button, manager: DialogManager):
|
||||
@@ -471,10 +487,7 @@ take_test_dialog = Dialog(
|
||||
on_click=on_single_answer_selected,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
Button(Const("➡️ Далее"), id="next", on_click=on_next_question),
|
||||
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_test),
|
||||
),
|
||||
Button(Const("➡️ Далее"), id="next", on_click=on_next_question),
|
||||
state=UserTestSG.question_single,
|
||||
getter=get_question_data,
|
||||
),
|
||||
@@ -490,24 +503,25 @@ take_test_dialog = Dialog(
|
||||
on_state_changed=on_multiple_answer_changed,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
Button(Const("➡️ Далее"), id="next", on_click=on_next_question),
|
||||
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_test),
|
||||
),
|
||||
Button(Const("➡️ Далее"), id="next", on_click=on_next_question),
|
||||
state=UserTestSG.question_multiple,
|
||||
getter=get_question_data,
|
||||
),
|
||||
Window(
|
||||
Format("{question_text}\n\n<i>Введите ответ:</i>"),
|
||||
MessageInput(on_text_answer_input),
|
||||
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_test),
|
||||
state=UserTestSG.question_input,
|
||||
getter=get_question_data,
|
||||
),
|
||||
Window(
|
||||
Format("{results_text}"),
|
||||
Column(
|
||||
Button(Const("📋 Подробные результаты"), id="detailed", on_click=on_show_detailed_results),
|
||||
Button(
|
||||
Const("📋 Подробные результаты"),
|
||||
id="detailed",
|
||||
on_click=on_show_detailed_results,
|
||||
when="are_results_viewable",
|
||||
),
|
||||
Button(Const("◀️ В главное меню"), id="back", on_click=on_back_to_menu),
|
||||
),
|
||||
state=UserTestSG.results,
|
||||
|
||||
@@ -35,6 +35,7 @@ class Test:
|
||||
expires_at: datetime | None = None
|
||||
attempts: int | None = None
|
||||
is_active: bool = True
|
||||
are_results_viewable: bool = False
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestDAO:
|
||||
|
||||
async def get_all(self) -> list[DomainTest]:
|
||||
result = await self.session.execute(
|
||||
select(Test).order_by(Test.id)
|
||||
select(Test).order_by(Test.created_at.desc())
|
||||
)
|
||||
models = list(result.scalars().all())
|
||||
return [TestDTO(model).to_domain() for model in models]
|
||||
@@ -35,6 +35,7 @@ class TestDAO:
|
||||
expires_at: datetime | None = None,
|
||||
attempts: int | None = None,
|
||||
is_active: bool = True,
|
||||
are_results_viewable: bool = False,
|
||||
) -> DomainTest:
|
||||
test = Test(
|
||||
title=title,
|
||||
@@ -44,6 +45,7 @@ class TestDAO:
|
||||
expires_at=expires_at,
|
||||
attempts=attempts,
|
||||
is_active=is_active,
|
||||
are_results_viewable=are_results_viewable,
|
||||
)
|
||||
self.session.add(test)
|
||||
await self.session.flush()
|
||||
@@ -60,6 +62,7 @@ class TestDAO:
|
||||
expires_at: datetime | None = None,
|
||||
attempts: int | None = None,
|
||||
is_active: bool | None = None,
|
||||
are_results_viewable: bool | None = None,
|
||||
) -> DomainTest | None:
|
||||
result = await self.session.execute(
|
||||
select(Test).where(Test.id == test_id)
|
||||
@@ -82,6 +85,8 @@ class TestDAO:
|
||||
test.attempts = attempts
|
||||
if is_active is not None:
|
||||
test.is_active = is_active
|
||||
if are_results_viewable is not None:
|
||||
test.are_results_viewable = are_results_viewable
|
||||
|
||||
await self.session.flush()
|
||||
await self.session.refresh(test)
|
||||
|
||||
@@ -20,7 +20,9 @@ class UserDAO:
|
||||
return UserDTO(model).to_domain() if model else None
|
||||
|
||||
async def get_all(self) -> list[DomainUser]:
|
||||
result = await self.session.execute(select(User))
|
||||
result = await self.session.execute(
|
||||
select(User).order_by(User.created_at.desc())
|
||||
)
|
||||
models = list(result.scalars().all())
|
||||
return [UserDTO(model).to_domain() for model in models]
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class TestDTO:
|
||||
expires_at=self.model.expires_at,
|
||||
attempts=self.model.attempts,
|
||||
is_active=self.model.is_active,
|
||||
are_results_viewable=self.model.are_results_viewable,
|
||||
created_at=self.model.created_at,
|
||||
updated_at=self.model.updated_at,
|
||||
)
|
||||
|
||||
@@ -63,6 +63,7 @@ class Test(Base):
|
||||
expires_at: Mapped[datetime | None] = mapped_column(default=None)
|
||||
attempts: Mapped[int | None] = mapped_column(Integer, default=None)
|
||||
is_active: Mapped[bool] = mapped_column(default=True)
|
||||
are_results_viewable: Mapped[bool] = mapped_column(default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user