This commit is contained in:
2026-01-04 01:39:35 +03:00
parent 05dd721f60
commit 1a8da5c070
10 changed files with 153 additions and 37 deletions
@@ -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:
continue
status_icon = "" if answer.is_correct else "" correct_options = [opt for opt in options if opt.is_correct]
correct_texts = [opt.text for opt in correct_options]
user_answer = answer.text_answer or "" status_icon = "" if answer.is_correct else ""
if "|" in user_answer:
user_answer = ", ".join(user_answer.split("|"))
lines.append(f"{status_icon} <b>Вопрос {i}</b>") user_answer = answer.text_answer or ""
lines.append(f"<blockquote>{question.text}</blockquote>") if "|" in user_answer:
lines.append(f"👤 <i>Ваш ответ:</i> {user_answer or ''}") user_answer = ", ".join(user_answer.split("|"))
lines.append(f"✓ <i>Правильно:</i> {', '.join(correct_texts)}\n")
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,
+1
View File
@@ -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())