mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 10:25:28 +03:00
commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
"""add_are_results_viewable_to_test
|
||||||
|
|
||||||
|
Revision ID: 40f5317720a4
|
||||||
|
Revises: e002f2b802ec
|
||||||
|
Create Date: 2026-01-04 01:29:18.257105
|
||||||
|
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = '40f5317720a4'
|
||||||
|
down_revision: str | None = 'e002f2b802ec'
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('tests', sa.Column('are_results_viewable', sa.Boolean(), nullable=False, server_default=sa.text('false')))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('tests', 'are_results_viewable')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -52,6 +52,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
|||||||
"test_info": "Тест не найден",
|
"test_info": "Тест не найден",
|
||||||
"is_active": False,
|
"is_active": False,
|
||||||
"button_text": "◀️ Назад",
|
"button_text": "◀️ Назад",
|
||||||
|
"results_button_text": "👁 Показать результаты",
|
||||||
}
|
}
|
||||||
|
|
||||||
test = await test_dao.get_by_id(test_id)
|
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": "Тест не найден",
|
"test_info": "Тест не найден",
|
||||||
"is_active": False,
|
"is_active": False,
|
||||||
"button_text": "◀️ Назад",
|
"button_text": "◀️ Назад",
|
||||||
|
"results_button_text": "👁 Показать результаты",
|
||||||
}
|
}
|
||||||
|
|
||||||
status = "🟢 Активен" if test.is_active else "🔴 Деактивирован"
|
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 "♾️ Без ограничений"
|
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 "📅 Без срока"
|
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 "👥 Для всех"
|
group_str = f"🎓 Группа {test.for_group}" if test.for_group else "👥 Для всех"
|
||||||
|
results_str = "👁 Результаты видны" if test.are_results_viewable else "🔒 Результаты скрыты"
|
||||||
|
|
||||||
test_info = (
|
test_info = (
|
||||||
f"<b>📝 Информация о тесте</b>\n\n"
|
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> {password_str}\n"
|
||||||
f"<b>Попытки:</b> {attempts_str}\n"
|
f"<b>Попытки:</b> {attempts_str}\n"
|
||||||
f"<b>Срок:</b> {expires_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 '—'}"
|
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 "🟢 Активировать"
|
button_text = "🔴 Деактивировать" if test.is_active else "🟢 Активировать"
|
||||||
|
results_button_text = "🔒 Скрыть результаты" if test.are_results_viewable else "👁 Показать результаты"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"test_info": test_info,
|
"test_info": test_info,
|
||||||
"is_active": test.is_active,
|
"is_active": test.is_active,
|
||||||
"button_text": button_text,
|
"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)
|
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):
|
async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: DialogManager):
|
||||||
await manager.switch_to(AdminTestsSG.tests_list)
|
await manager.switch_to(AdminTestsSG.tests_list)
|
||||||
|
|
||||||
@@ -435,6 +457,11 @@ tests_dialog = Dialog(
|
|||||||
id="toggle_active",
|
id="toggle_active",
|
||||||
on_click=on_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="statistics", on_click=on_statistics),
|
||||||
Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test),
|
Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test),
|
||||||
Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu),
|
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": "Тест не найден",
|
"test_info": "Тест не найден",
|
||||||
"is_active": False,
|
"is_active": False,
|
||||||
"button_text": "◀️ Назад",
|
"button_text": "◀️ Назад",
|
||||||
|
"results_button_text": "👁 Показать результаты",
|
||||||
}
|
}
|
||||||
|
|
||||||
test = await test_dao.get_by_id(test_id)
|
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": "Тест не найден",
|
"test_info": "Тест не найден",
|
||||||
"is_active": False,
|
"is_active": False,
|
||||||
"button_text": "◀️ Назад",
|
"button_text": "◀️ Назад",
|
||||||
|
"results_button_text": "👁 Показать результаты",
|
||||||
}
|
}
|
||||||
|
|
||||||
status = "🟢 Активен" if test.is_active else "🔴 Деактивирован"
|
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 "♾️ Без ограничений"
|
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 "📅 Без срока"
|
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 "👥 Для всех"
|
group_str = f"🎓 Группа {test.for_group}" if test.for_group else "👥 Для всех"
|
||||||
|
results_str = "👁 Результаты видны" if test.are_results_viewable else "🔒 Результаты скрыты"
|
||||||
|
|
||||||
test_info = (
|
test_info = (
|
||||||
f"<b>📝 Информация о тесте</b>\n\n"
|
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> {password_str}\n"
|
||||||
f"<b>Попытки:</b> {attempts_str}\n"
|
f"<b>Попытки:</b> {attempts_str}\n"
|
||||||
f"<b>Срок:</b> {expires_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 '—'}"
|
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 "🟢 Активировать"
|
button_text = "🔴 Деактивировать" if test.is_active else "🟢 Активировать"
|
||||||
|
results_button_text = "🔒 Скрыть результаты" if test.are_results_viewable else "👁 Показать результаты"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"test_info": test_info,
|
"test_info": test_info,
|
||||||
"is_active": test.is_active,
|
"is_active": test.is_active,
|
||||||
"button_text": button_text,
|
"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)
|
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):
|
async def on_back_to_list(_callback: CallbackQuery, _button: Button, manager: DialogManager):
|
||||||
await manager.switch_to(CreatorTestsSG.tests_list)
|
await manager.switch_to(CreatorTestsSG.tests_list)
|
||||||
|
|
||||||
@@ -439,6 +461,11 @@ tests_dialog = Dialog(
|
|||||||
id="toggle_active",
|
id="toggle_active",
|
||||||
on_click=on_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="statistics", on_click=on_statistics),
|
||||||
Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test),
|
Button(Const("🔗 Поделиться"), id="share", on_click=on_share_test),
|
||||||
Button(Const("✏️ Изменить"), id="edit_menu", on_click=on_edit_menu),
|
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, _ = await test_repo.get_test_with_questions(attempt.test_id)
|
||||||
test_title = test.title if test else "Неизвестный тест"
|
test_title = test.title if test else "Неизвестный тест"
|
||||||
|
are_results_viewable = test.are_results_viewable if test else False
|
||||||
|
|
||||||
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
||||||
finished_at_msk = to_msk(attempt.finished_at)
|
finished_at_msk = to_msk(attempt.finished_at)
|
||||||
date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else "—"
|
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 = [
|
lines = [
|
||||||
f"<b>📝 {test_title}</b>\n",
|
f"<b>📝 {test_title}</b>\n",
|
||||||
f"📊 <b>Результат:</b> {attempt.score}%",
|
f"📊 <b>Результат:</b> {attempt.score}%",
|
||||||
|
f"✏️ <b>Правильных ответов:</b> {correct_count} из {total_count}",
|
||||||
f"📅 <b>Дата:</b> {date_str}",
|
f"📅 <b>Дата:</b> {date_str}",
|
||||||
f"🏆 <b>Статус:</b> {status}\n",
|
f"🏆 <b>Статус:</b> {status}",
|
||||||
"<b>📋 Ответы:</b>\n",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for i, answer in enumerate(answers, 1):
|
if are_results_viewable:
|
||||||
question, options = await test_repo.get_question_with_options(answer.question_id)
|
lines.append("\n<b>📋 Ответы:</b>\n")
|
||||||
if not question:
|
|
||||||
continue
|
|
||||||
|
|
||||||
correct_options = [opt for opt in options if opt.is_correct]
|
for i, answer in enumerate(answers, 1):
|
||||||
correct_texts = [opt.text for opt in correct_options]
|
question, options = await test_repo.get_question_with_options(answer.question_id)
|
||||||
|
if not question:
|
||||||
status_icon = "✅" if answer.is_correct else "❌"
|
continue
|
||||||
|
|
||||||
user_answer = answer.text_answer or ""
|
correct_options = [opt for opt in options if opt.is_correct]
|
||||||
if "|" in user_answer:
|
correct_texts = [opt.text for opt in correct_options]
|
||||||
user_answer = ", ".join(user_answer.split("|"))
|
|
||||||
|
status_icon = "✅" if answer.is_correct else "❌"
|
||||||
lines.append(f"{status_icon} <b>Вопрос {i}</b>")
|
|
||||||
lines.append(f"<blockquote>{question.text}</blockquote>")
|
user_answer = answer.text_answer or ""
|
||||||
lines.append(f"👤 <i>Ваш ответ:</i> {user_answer or '—'}")
|
if "|" in user_answer:
|
||||||
lines.append(f"✓ <i>Правильно:</i> {', '.join(correct_texts)}\n")
|
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)}
|
return {"result_info": "\n".join(lines)}
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ async def on_text_answer_input(
|
|||||||
test_repo: FromDishka[TestRepository],
|
test_repo: FromDishka[TestRepository],
|
||||||
attempt_repo: FromDishka[TestAttemptRepository],
|
attempt_repo: FromDishka[TestAttemptRepository],
|
||||||
answer_dao: FromDishka[UserAnswerDAO],
|
answer_dao: FromDishka[UserAnswerDAO],
|
||||||
|
test_dao: FromDishka[TestDAO],
|
||||||
):
|
):
|
||||||
start_data = manager.start_data or {}
|
start_data = manager.start_data or {}
|
||||||
assert isinstance(start_data, dict)
|
assert isinstance(start_data, dict)
|
||||||
@@ -248,6 +249,7 @@ async def on_text_answer_input(
|
|||||||
question_id = questions[current_index]
|
question_id = questions[current_index]
|
||||||
text_answer = message.text.strip() if message.text else ""
|
text_answer = message.text.strip() if message.text else ""
|
||||||
attempt_id = manager.dialog_data.get("attempt_id") or start_data.get("attempt_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:
|
if not attempt_id:
|
||||||
await message.answer("❌ Ошибка попытки")
|
await message.answer("❌ Ошибка попытки")
|
||||||
@@ -270,7 +272,9 @@ async def on_text_answer_input(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if current_index + 1 >= len(questions):
|
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:
|
else:
|
||||||
next_index = current_index + 1
|
next_index = current_index + 1
|
||||||
manager.dialog_data["current_question_index"] = next_index
|
manager.dialog_data["current_question_index"] = next_index
|
||||||
@@ -290,6 +294,7 @@ async def on_next_question(
|
|||||||
test_repo: FromDishka[TestRepository],
|
test_repo: FromDishka[TestRepository],
|
||||||
attempt_repo: FromDishka[TestAttemptRepository],
|
attempt_repo: FromDishka[TestAttemptRepository],
|
||||||
answer_dao: FromDishka[UserAnswerDAO],
|
answer_dao: FromDishka[UserAnswerDAO],
|
||||||
|
test_dao: FromDishka[TestDAO],
|
||||||
):
|
):
|
||||||
start_data = manager.start_data or {}
|
start_data = manager.start_data or {}
|
||||||
assert isinstance(start_data, dict)
|
assert isinstance(start_data, dict)
|
||||||
@@ -311,6 +316,7 @@ async def on_next_question(
|
|||||||
|
|
||||||
answer_data = user_answers[str(question_id)]
|
answer_data = user_answers[str(question_id)]
|
||||||
attempt_id = manager.dialog_data.get("attempt_id") or start_data.get("attempt_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:
|
if not attempt_id:
|
||||||
await _callback.answer("❌ Ошибка попытки")
|
await _callback.answer("❌ Ошибка попытки")
|
||||||
@@ -352,7 +358,9 @@ async def on_next_question(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if current_index + 1 >= len(questions):
|
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:
|
else:
|
||||||
next_index = current_index + 1
|
next_index = current_index + 1
|
||||||
manager.dialog_data["current_question_index"] = next_index
|
manager.dialog_data["current_question_index"] = next_index
|
||||||
@@ -364,7 +372,13 @@ async def on_next_question(
|
|||||||
await manager.switch_to(next_state)
|
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)
|
correct_count = await attempt_repo.calculate_attempt_score(attempt_id)
|
||||||
|
|
||||||
score = round((correct_count / total_questions * 100)) if total_questions > 0 else 0
|
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["correct_count"] = correct_count
|
||||||
manager.dialog_data["total_questions"] = total_questions
|
manager.dialog_data["total_questions"] = total_questions
|
||||||
manager.dialog_data["is_passed"] = is_passed
|
manager.dialog_data["is_passed"] = is_passed
|
||||||
|
manager.dialog_data["are_results_viewable"] = are_results_viewable
|
||||||
|
|
||||||
await manager.switch_to(UserTestSG.results)
|
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)
|
correct_count = dialog_manager.dialog_data.get("correct_count", 0)
|
||||||
total_questions = dialog_manager.dialog_data.get("total_questions", 0)
|
total_questions = dialog_manager.dialog_data.get("total_questions", 0)
|
||||||
is_passed = dialog_manager.dialog_data.get("is_passed", False)
|
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:
|
if is_passed:
|
||||||
status = "✅ <b>Тест пройден!</b>"
|
status = "✅ <b>Тест пройден!</b>"
|
||||||
@@ -397,7 +413,7 @@ async def get_results_data(dialog_manager: DialogManager, **_kwargs):
|
|||||||
f"✏️ <b>Правильных ответов:</b> {correct_count} из {total_questions}"
|
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):
|
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,
|
on_click=on_single_answer_selected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Button(Const("➡️ Далее"), id="next", on_click=on_next_question),
|
||||||
Button(Const("➡️ Далее"), id="next", on_click=on_next_question),
|
|
||||||
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_test),
|
|
||||||
),
|
|
||||||
state=UserTestSG.question_single,
|
state=UserTestSG.question_single,
|
||||||
getter=get_question_data,
|
getter=get_question_data,
|
||||||
),
|
),
|
||||||
@@ -490,24 +503,25 @@ take_test_dialog = Dialog(
|
|||||||
on_state_changed=on_multiple_answer_changed,
|
on_state_changed=on_multiple_answer_changed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Button(Const("➡️ Далее"), id="next", on_click=on_next_question),
|
||||||
Button(Const("➡️ Далее"), id="next", on_click=on_next_question),
|
|
||||||
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_test),
|
|
||||||
),
|
|
||||||
state=UserTestSG.question_multiple,
|
state=UserTestSG.question_multiple,
|
||||||
getter=get_question_data,
|
getter=get_question_data,
|
||||||
),
|
),
|
||||||
Window(
|
Window(
|
||||||
Format("{question_text}\n\n<i>Введите ответ:</i>"),
|
Format("{question_text}\n\n<i>Введите ответ:</i>"),
|
||||||
MessageInput(on_text_answer_input),
|
MessageInput(on_text_answer_input),
|
||||||
Button(Const("❌ Отмена"), id="cancel", on_click=on_cancel_test),
|
|
||||||
state=UserTestSG.question_input,
|
state=UserTestSG.question_input,
|
||||||
getter=get_question_data,
|
getter=get_question_data,
|
||||||
),
|
),
|
||||||
Window(
|
Window(
|
||||||
Format("{results_text}"),
|
Format("{results_text}"),
|
||||||
Column(
|
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),
|
Button(Const("◀️ В главное меню"), id="back", on_click=on_back_to_menu),
|
||||||
),
|
),
|
||||||
state=UserTestSG.results,
|
state=UserTestSG.results,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class Test:
|
|||||||
expires_at: datetime | None = None
|
expires_at: datetime | None = None
|
||||||
attempts: int | None = None
|
attempts: int | None = None
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
|
are_results_viewable: bool = False
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
updated_at: datetime | None = None
|
updated_at: datetime | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class TestDAO:
|
|||||||
|
|
||||||
async def get_all(self) -> list[DomainTest]:
|
async def get_all(self) -> list[DomainTest]:
|
||||||
result = await self.session.execute(
|
result = await self.session.execute(
|
||||||
select(Test).order_by(Test.id)
|
select(Test).order_by(Test.created_at.desc())
|
||||||
)
|
)
|
||||||
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]
|
||||||
@@ -35,6 +35,7 @@ class TestDAO:
|
|||||||
expires_at: datetime | None = None,
|
expires_at: datetime | None = None,
|
||||||
attempts: int | None = None,
|
attempts: int | None = None,
|
||||||
is_active: bool = True,
|
is_active: bool = True,
|
||||||
|
are_results_viewable: bool = False,
|
||||||
) -> DomainTest:
|
) -> DomainTest:
|
||||||
test = Test(
|
test = Test(
|
||||||
title=title,
|
title=title,
|
||||||
@@ -44,6 +45,7 @@ class TestDAO:
|
|||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
attempts=attempts,
|
attempts=attempts,
|
||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
|
are_results_viewable=are_results_viewable,
|
||||||
)
|
)
|
||||||
self.session.add(test)
|
self.session.add(test)
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
@@ -60,6 +62,7 @@ class TestDAO:
|
|||||||
expires_at: datetime | None = None,
|
expires_at: datetime | None = None,
|
||||||
attempts: int | None = None,
|
attempts: int | None = None,
|
||||||
is_active: bool | None = None,
|
is_active: bool | None = None,
|
||||||
|
are_results_viewable: bool | None = None,
|
||||||
) -> 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)
|
||||||
@@ -82,6 +85,8 @@ class TestDAO:
|
|||||||
test.attempts = attempts
|
test.attempts = attempts
|
||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
test.is_active = is_active
|
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.flush()
|
||||||
await self.session.refresh(test)
|
await self.session.refresh(test)
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ class UserDAO:
|
|||||||
return UserDTO(model).to_domain() if model else None
|
return UserDTO(model).to_domain() if model else None
|
||||||
|
|
||||||
async def get_all(self) -> list[DomainUser]:
|
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())
|
models = list(result.scalars().all())
|
||||||
return [UserDTO(model).to_domain() for model in models]
|
return [UserDTO(model).to_domain() for model in models]
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class TestDTO:
|
|||||||
expires_at=self.model.expires_at,
|
expires_at=self.model.expires_at,
|
||||||
attempts=self.model.attempts,
|
attempts=self.model.attempts,
|
||||||
is_active=self.model.is_active,
|
is_active=self.model.is_active,
|
||||||
|
are_results_viewable=self.model.are_results_viewable,
|
||||||
created_at=self.model.created_at,
|
created_at=self.model.created_at,
|
||||||
updated_at=self.model.updated_at,
|
updated_at=self.model.updated_at,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class Test(Base):
|
|||||||
expires_at: Mapped[datetime | None] = mapped_column(default=None)
|
expires_at: Mapped[datetime | None] = mapped_column(default=None)
|
||||||
attempts: Mapped[int | None] = mapped_column(Integer, default=None)
|
attempts: Mapped[int | None] = mapped_column(Integer, default=None)
|
||||||
is_active: Mapped[bool] = mapped_column(default=True)
|
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())
|
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
|
||||||
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
|
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user