This commit is contained in:
2026-01-06 19:35:49 +03:00
parent ff92ab2b30
commit 7d2a734b7d
13 changed files with 162 additions and 9 deletions
@@ -0,0 +1,26 @@
"""add time_limit to test
Revision ID: b1c2d3e4f5a6
Revises: ca107b03ddf8
Create Date: 2026-01-06 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b1c2d3e4f5a6'
down_revision: Union[str, None] = 'ca107b03ddf8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('tests', sa.Column('time_limit', sa.Integer(), nullable=True))
def downgrade() -> None:
op.drop_column('tests', 'time_limit')
+6 -7
View File
@@ -1,14 +1,13 @@
[bot] [bot]
token = "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz" token = "1234567890"
creator_id = 123456789 creator_id = 1234567890
[security] [security]
test_hash_salt = "your_secret_salt_here_change_in_production" encode_key = "encode_key"
test_hash_length = 16
[database] [database]
host = "localhost" host = "localhost"
port = 5432 port = 5432
user = "trudex_user" user = "postgres"
password = "secure_password" password = "passkey"
database = "trudex_db" database = "trudex"
@@ -100,11 +100,41 @@ async def on_attempts_input(message: Message, _widget: MessageInput, manager: Di
return return
manager.dialog_data["attempts"] = attempts manager.dialog_data["attempts"] = attempts
await manager.switch_to(SharedCreateTestSG.input_expires_at) await manager.switch_to(SharedCreateTestSG.input_time_limit)
async def on_skip_attempts(_callback: CallbackQuery, _button: Button, manager: DialogManager): async def on_skip_attempts(_callback: CallbackQuery, _button: Button, manager: DialogManager):
manager.dialog_data["attempts"] = None manager.dialog_data["attempts"] = None
await manager.switch_to(SharedCreateTestSG.input_time_limit)
async def on_time_limit_input(message: Message, _widget: MessageInput, manager: DialogManager):
if not message.text:
await message.answer("❌ Лимит времени не может быть пустым")
return
time_limit_str = message.text.strip()
if not time_limit_str.isdigit():
await message.answer("❌ Лимит времени должен быть числом (в минутах)")
return
time_limit_minutes = int(time_limit_str)
if time_limit_minutes < 1:
await message.answer("❌ Лимит времени должен быть больше 0")
return
if time_limit_minutes > 1440:
await message.answer("❌ Лимит времени не может быть больше 1440 минут (24 часа)")
return
manager.dialog_data["time_limit"] = time_limit_minutes * 60
await manager.switch_to(SharedCreateTestSG.input_expires_at)
async def on_skip_time_limit(_callback: CallbackQuery, _button: Button, manager: DialogManager):
manager.dialog_data["time_limit"] = None
await manager.switch_to(SharedCreateTestSG.input_expires_at) await manager.switch_to(SharedCreateTestSG.input_expires_at)
@@ -139,11 +169,13 @@ async def get_test_info(dialog_manager: DialogManager, **_kwargs):
description = dialog_manager.dialog_data.get("description", "") description = dialog_manager.dialog_data.get("description", "")
password = dialog_manager.dialog_data.get("password") password = dialog_manager.dialog_data.get("password")
attempts = dialog_manager.dialog_data.get("attempts") attempts = dialog_manager.dialog_data.get("attempts")
time_limit = dialog_manager.dialog_data.get("time_limit")
expires_at = dialog_manager.dialog_data.get("expires_at") expires_at = dialog_manager.dialog_data.get("expires_at")
for_group = dialog_manager.dialog_data.get("for_group") for_group = dialog_manager.dialog_data.get("for_group")
password_str = f"🔒 {password}" if password else "Без пароля" password_str = f"🔒 {password}" if password else "Без пароля"
attempts_str = f"🔄 {attempts}" if attempts else "♾️ Без ограничений" attempts_str = f"🔄 {attempts}" if attempts else "♾️ Без ограничений"
time_limit_str = f"⏱️ {time_limit // 60} мин" if time_limit else "⏱️ Без лимита"
expires_at_msk = to_msk(expires_at) expires_at_msk = to_msk(expires_at)
expires_str = expires_at_msk.strftime("%d.%m.%Y") if expires_at_msk else "Без срока" expires_str = expires_at_msk.strftime("%d.%m.%Y") if expires_at_msk else "Без срока"
group_str = str(for_group) if for_group else "Для всех" group_str = str(for_group) if for_group else "Для всех"
@@ -155,6 +187,7 @@ async def get_test_info(dialog_manager: DialogManager, **_kwargs):
f"<b>Описание:</b> {description}\n" f"<b>Описание:</b> {description}\n"
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> {time_limit_str}\n"
f"<b>Истекает:</b> {expires_str}\n" f"<b>Истекает:</b> {expires_str}\n"
f"<b>Для группы:</b> {group_str}" f"<b>Для группы:</b> {group_str}"
) )
@@ -168,6 +201,7 @@ async def on_confirm_test(_callback: CallbackQuery, _button: Button, manager: Di
description = manager.dialog_data.get("description") description = manager.dialog_data.get("description")
password = manager.dialog_data.get("password") password = manager.dialog_data.get("password")
attempts = manager.dialog_data.get("attempts") attempts = manager.dialog_data.get("attempts")
time_limit = manager.dialog_data.get("time_limit")
expires_at = manager.dialog_data.get("expires_at") expires_at = manager.dialog_data.get("expires_at")
for_group = manager.dialog_data.get("for_group") for_group = manager.dialog_data.get("for_group")
@@ -176,6 +210,7 @@ async def on_confirm_test(_callback: CallbackQuery, _button: Button, manager: Di
description=description, description=description,
password=password, password=password,
attempts=attempts, attempts=attempts,
time_limit=time_limit,
expires_at=expires_at, expires_at=expires_at,
for_group=for_group, for_group=for_group,
) )
@@ -470,6 +505,12 @@ shared_create_test_dialog = Dialog(
Button(Const("⏭️ Без ограничений"), id="skip_attempts", on_click=on_skip_attempts), Button(Const("⏭️ Без ограничений"), id="skip_attempts", on_click=on_skip_attempts),
state=SharedCreateTestSG.input_attempts, state=SharedCreateTestSG.input_attempts,
), ),
Window(
Const("<b>⏱️ Лимит времени</b>\n\n🔢 <b>Введите лимит времени в минутах</b> (1-1440) или пропустите для неограниченного времени:"),
MessageInput(on_time_limit_input),
Button(Const("⏭️ Без лимита"), id="skip_time_limit", on_click=on_skip_time_limit),
state=SharedCreateTestSG.input_time_limit,
),
Window( Window(
Const("<b>📅 Срок действия</b>\n\n🗓 <b>Выберите дату истечения теста</b> или пропустите:"), Const("<b>📅 Срок действия</b>\n\n🗓 <b>Выберите дату истечения теста</b> или пропустите:"),
Calendar(id="calendar", on_click=on_date_selected), Calendar(id="calendar", on_click=on_date_selected),
@@ -15,6 +15,7 @@ class SharedTestsSG(StatesGroup):
edit_menu = State() edit_menu = State()
edit_password = State() edit_password = State()
edit_attempts = State() edit_attempts = State()
edit_time_limit = State()
edit_group = State() edit_group = State()
edit_expires = State() edit_expires = State()
statistics = State() statistics = State()
@@ -38,6 +39,7 @@ class SharedCreateTestSG(StatesGroup):
input_description = State() input_description = State()
input_password = State() input_password = State()
input_attempts = State() input_attempts = State()
input_time_limit = State()
input_expires_at = State() input_expires_at = State()
input_for_group = State() input_for_group = State()
confirm_test_info = State() confirm_test_info = State()
@@ -36,6 +36,7 @@ SPEC_INFO = """<b>📋 Спецификация формата JSON</b>
"description": "Описание теста", "description": "Описание теста",
"password": null, "password": null,
"attempts": null, "attempts": null,
"time_limit": null,
"expires_at": null, "expires_at": null,
"for_group": null, "for_group": null,
"questions": [...] "questions": [...]
@@ -46,6 +47,7 @@ SPEC_INFO = """<b>📋 Спецификация формата JSON</b>
• <code>description</code> — описание (до 2000 символов) • <code>description</code> — описание (до 2000 символов)
• <code>password</code> — пароль для доступа или <code>null</code> • <code>password</code> — пароль для доступа или <code>null</code>
• <code>attempts</code> — лимит попыток (1-100) или <code>null</code> • <code>attempts</code> — лимит попыток (1-100) или <code>null</code>
• <code>time_limit</code> — лимит времени в секундах (1-86400) или <code>null</code>
• <code>expires_at</code> — срок действия в ISO формате или <code>null</code> • <code>expires_at</code> — срок действия в ISO формате или <code>null</code>
• <code>for_group</code> — номер группы или <code>null</code> для всех • <code>for_group</code> — номер группы или <code>null</code> для всех
@@ -90,6 +92,7 @@ TEMPLATE_ULTIMATE = """// ══════════════════
// ⚙️ НАСТРОЙКИ: // ⚙️ НАСТРОЙКИ:
// • Пароль: test2024 // • Пароль: test2024
// • Попыток: 5 // • Попыток: 5
// • Лимит времени: 1800 секунд (30 минут)
// • Срок действия: 31 декабря 2026, 23:59 // • Срок действия: 31 декабря 2026, 23:59
// • Для группы: 2024 (или null для всех) // • Для группы: 2024 (или null для всех)
// //
@@ -104,6 +107,7 @@ TEMPLATE_ULTIMATE = """// ══════════════════
// 💡 ПОДСКАЗКИ: // 💡 ПОДСКАЗКИ:
// • null означает "не задано" / "без ограничений" // • null означает "не задано" / "без ограничений"
// • expires_at в формате ISO 8601: YYYY-MM-DDTHH:MM:SS // • expires_at в формате ISO 8601: YYYY-MM-DDTHH:MM:SS
// • time_limit в секундах (1-86400), null для без ограничений
// • for_group - номер группы или null для всех пользователей // • for_group - номер группы или null для всех пользователей
// • image_url - URL изображения к вопросу (опционально) // • image_url - URL изображения к вопросу (опционально)
// //
@@ -114,6 +118,7 @@ TEMPLATE_ULTIMATE = """// ══════════════════
"description": "Полная демонстрация всех возможностей формата: разные типы вопросов, настройки доступа, ограничения по времени и группам", "description": "Полная демонстрация всех возможностей формата: разные типы вопросов, настройки доступа, ограничения по времени и группам",
"password": "test2024", "password": "test2024",
"attempts": 5, "attempts": 5,
"time_limit": 1800,
"expires_at": "2026-12-31T23:59:59", "expires_at": "2026-12-31T23:59:59",
"for_group": 2024, "for_group": 2024,
"questions": [ "questions": [
@@ -225,6 +230,7 @@ async def on_test_selected_for_export(
"description": test.description, "description": test.description,
"password": test.password, "password": test.password,
"attempts": test.attempts, "attempts": test.attempts,
"time_limit": test.time_limit,
"expires_at": test.expires_at.isoformat() if test.expires_at else None, "expires_at": test.expires_at.isoformat() if test.expires_at else None,
"for_group": test.for_group, "for_group": test.for_group,
"questions": [], "questions": [],
@@ -340,6 +346,7 @@ async def create_test_from_parsed(
description=parsed.description, description=parsed.description,
password=parsed.password, password=parsed.password,
attempts=parsed.attempts, attempts=parsed.attempts,
time_limit=parsed.time_limit,
expires_at=parsed.expires_at, expires_at=parsed.expires_at,
for_group=parsed.for_group, for_group=parsed.for_group,
is_active=False, is_active=False,
@@ -68,6 +68,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
status = "🟢 Активен" if test.is_active else "🔴 Деактивирован" status = "🟢 Активен" if test.is_active else "🔴 Деактивирован"
password_str = f"🔒 {test.password}" if test.password else "🔓 Без пароля" password_str = f"🔒 {test.password}" if test.password else "🔓 Без пароля"
attempts_str = f"🔄 {test.attempts}" if test.attempts else "♾️ Без ограничений" attempts_str = f"🔄 {test.attempts}" if test.attempts else "♾️ Без ограничений"
time_limit_str = f"⏱️ {test.time_limit // 60} мин" if test.time_limit 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 "🔒 Результаты скрыты" results_str = "👁 Результаты видны" if test.are_results_viewable else "🔒 Результаты скрыты"
@@ -80,6 +81,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
f"<b>Вопросов:</b> {questions_count}\n" f"<b>Вопросов:</b> {questions_count}\n"
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> {time_limit_str}\n"
f"<b>Срок:</b> {expires_str}\n" f"<b>Срок:</b> {expires_str}\n"
f"<b>Группа:</b> {group_str}\n" f"<b>Группа:</b> {group_str}\n"
f"<b>Видимость:</b> {results_str}\n\n" f"<b>Видимость:</b> {results_str}\n\n"
@@ -254,6 +256,7 @@ async def on_export_test(
"description": test.description, "description": test.description,
"password": test.password, "password": test.password,
"attempts": test.attempts, "attempts": test.attempts,
"time_limit": test.time_limit,
"expires_at": test.expires_at.isoformat() if test.expires_at else None, "expires_at": test.expires_at.isoformat() if test.expires_at else None,
"for_group": test.for_group, "for_group": test.for_group,
"questions": [], "questions": [],
@@ -361,6 +364,10 @@ async def on_edit_attempts(_callback: CallbackQuery, _button: Button, manager: D
await manager.switch_to(SharedTestsSG.edit_attempts) await manager.switch_to(SharedTestsSG.edit_attempts)
async def on_edit_time_limit(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(SharedTestsSG.edit_time_limit)
async def on_edit_group(_callback: CallbackQuery, _button: Button, manager: DialogManager): async def on_edit_group(_callback: CallbackQuery, _button: Button, manager: DialogManager):
await manager.switch_to(SharedTestsSG.edit_group) await manager.switch_to(SharedTestsSG.edit_group)
@@ -446,6 +453,51 @@ async def on_remove_attempts(_callback: CallbackQuery, _button: Button, manager:
await manager.switch_to(SharedTestsSG.test_detail) await manager.switch_to(SharedTestsSG.test_detail)
@inject
async def on_time_limit_input(message: Message, _widget: MessageInput, manager: DialogManager, test_dao: FromDishka[TestDAO]):
test_id = manager.dialog_data.get("selected_test_id")
if not test_id:
await message.answer("❌ Тест не найден")
return
if not message.text:
await message.answer("❌ Лимит времени не может быть пустым")
return
time_limit_str = message.text.strip()
if not time_limit_str.isdigit():
await message.answer("❌ Лимит времени должен быть числом (в минутах)")
return
time_limit_minutes = int(time_limit_str)
if time_limit_minutes < 1:
await message.answer("❌ Лимит времени должен быть больше 0")
return
if time_limit_minutes > 1440:
await message.answer("❌ Лимит времени не может быть больше 1440 минут (24 часа)")
return
time_limit_seconds = time_limit_minutes * 60
await test_dao.update(test_id, time_limit=time_limit_seconds)
await message.answer("✅ Лимит времени обновлен")
await manager.switch_to(SharedTestsSG.test_detail)
@inject
async def on_remove_time_limit(_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
await test_dao.update(test_id, time_limit=None)
await _callback.answer("✅ Лимит времени удален")
await manager.switch_to(SharedTestsSG.test_detail)
@inject @inject
async def get_groups_for_edit(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs): async def get_groups_for_edit(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs):
groups = await group_dao.get_all() groups = await group_dao.get_all()
@@ -561,6 +613,7 @@ shared_tests_dialog = Dialog(
Column( Column(
Button(Const("🔑 Пароль"), id="edit_password", on_click=on_edit_password), Button(Const("🔑 Пароль"), id="edit_password", on_click=on_edit_password),
Button(Const("🔄 Попытки"), id="edit_attempts", on_click=on_edit_attempts), Button(Const("🔄 Попытки"), id="edit_attempts", on_click=on_edit_attempts),
Button(Const("⏱️ Лимит времени"), id="edit_time_limit", on_click=on_edit_time_limit),
Button(Const("👥 Группа"), id="edit_group", on_click=on_edit_group), Button(Const("👥 Группа"), id="edit_group", on_click=on_edit_group),
Button(Const("📅 Срок действия"), id="edit_expires", on_click=on_edit_expires), Button(Const("📅 Срок действия"), id="edit_expires", on_click=on_edit_expires),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail), Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail),
@@ -585,6 +638,15 @@ shared_tests_dialog = Dialog(
), ),
state=SharedTestsSG.edit_attempts, state=SharedTestsSG.edit_attempts,
), ),
Window(
Const("<b>⏱️ Изменение лимита времени</b>\n\n🔢 <b>Введите лимит времени в минутах</b> (1-1440) или удалите ограничение:"),
MessageInput(on_time_limit_input),
Column(
Button(Const("🗑 Без лимита"), id="remove_time_limit", on_click=on_remove_time_limit),
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_edit_menu),
),
state=SharedTestsSG.edit_time_limit,
),
Window( Window(
Const("<b>👥 Изменение группы</b>\n\n🎓 <b>Выберите группу</b> или удалите привязку:"), Const("<b>👥 Изменение группы</b>\n\n🎓 <b>Выберите группу</b> или удалите привязку:"),
ScrollingGroup( ScrollingGroup(
@@ -41,13 +41,15 @@ async def get_deeplink_test_data(
password_str = "🔒 Требуется пароль" if test.password else "🔓 Без пароля" password_str = "🔒 Требуется пароль" if test.password else "🔓 Без пароля"
attempts_str = f"🔄 Попыток: {test.attempts}" if test.attempts else "🔄 Попыток: ♾️" attempts_str = f"🔄 Попыток: {test.attempts}" if test.attempts else "🔄 Попыток: ♾️"
time_limit_str = f"⏱️ Время: {test.time_limit // 60} мин" if test.time_limit else "⏱️ Без лимита"
test_info = ( test_info = (
f"<b>📝 {test.title}</b>\n\n" f"<b>📝 {test.title}</b>\n\n"
f"<blockquote>{test.description or ''}</blockquote>\n\n" f"<blockquote>{test.description or ''}</blockquote>\n\n"
f"<b>Вопросов:</b> {questions_count}\n" f"<b>Вопросов:</b> {questions_count}\n"
f"{password_str}\n" f"{password_str}\n"
f"{attempts_str}" f"{attempts_str}\n"
f"{time_limit_str}"
) )
return {"test_info": test_info, "can_start": True, "has_password": bool(test.password)} return {"test_info": test_info, "can_start": True, "has_password": bool(test.password)}
@@ -278,6 +278,7 @@ async def get_test_detail(
password_str = "🔒 Требуется пароль" if test.password else "🔓 Без пароля" password_str = "🔒 Требуется пароль" if test.password else "🔓 Без пароля"
attempts_str = f"🔄 Попыток: {len(finished_attempts)}/{test.attempts}" if test.attempts else f"🔄 Попыток: {len(finished_attempts)}/♾️" attempts_str = f"🔄 Попыток: {len(finished_attempts)}/{test.attempts}" if test.attempts else f"🔄 Попыток: {len(finished_attempts)}/♾️"
time_limit_str = f"⏱️ Время: {test.time_limit // 60} мин" if test.time_limit else "⏱️ Без лимита"
expires_at_msk = to_msk(test.expires_at) expires_at_msk = to_msk(test.expires_at)
expires_str = f"📅 До {expires_at_msk.strftime('%d.%m.%Y %H:%M')}" if expires_at_msk else "📅 Без срока" expires_str = f"📅 До {expires_at_msk.strftime('%d.%m.%Y %H:%M')}" if expires_at_msk else "📅 Без срока"
@@ -289,6 +290,7 @@ async def get_test_detail(
f"<b>Вопросов:</b> {len(questions)}\n" f"<b>Вопросов:</b> {len(questions)}\n"
f"{password_str}\n" f"{password_str}\n"
f"{attempts_str}\n" f"{attempts_str}\n"
f"{time_limit_str}\n"
f"{expires_str}\n" f"{expires_str}\n"
f"{group_str}" f"{group_str}"
) )
+1
View File
@@ -41,6 +41,7 @@ class Test:
password: str | None = None password: str | None = None
expires_at: datetime | None = None expires_at: datetime | None = None
attempts: int | None = None attempts: int | None = None
time_limit: int | None = None
is_active: bool = True is_active: bool = True
are_results_viewable: bool = False are_results_viewable: bool = False
created_at: datetime | None = None created_at: datetime | None = None
+3
View File
@@ -24,6 +24,7 @@ class ParsedTest:
description: str | None description: str | None
password: str | None password: str | None
attempts: int | None attempts: int | None
time_limit: int | None
expires_at: datetime | None expires_at: datetime | None
for_group: int | None for_group: int | None
questions: list[ParsedQuestion] questions: list[ParsedQuestion]
@@ -53,6 +54,7 @@ class TestParser:
description = self._parse_string(data, "description", required=False, max_length=2000, errors=errors) description = self._parse_string(data, "description", required=False, max_length=2000, errors=errors)
password = self._parse_string(data, "password", required=False, max_length=255, errors=errors) password = self._parse_string(data, "password", required=False, max_length=255, errors=errors)
attempts = self._parse_int(data, "attempts", required=False, min_val=1, max_val=100, errors=errors) attempts = self._parse_int(data, "attempts", required=False, min_val=1, max_val=100, errors=errors)
time_limit = self._parse_int(data, "time_limit", required=False, min_val=1, max_val=86400, errors=errors)
expires_at = self._parse_datetime(data, "expires_at", required=False, errors=errors) expires_at = self._parse_datetime(data, "expires_at", required=False, errors=errors)
for_group = self._parse_int(data, "for_group", required=False, errors=errors) for_group = self._parse_int(data, "for_group", required=False, errors=errors)
@@ -68,6 +70,7 @@ class TestParser:
description=description, description=description,
password=password, password=password,
attempts=attempts, attempts=attempts,
time_limit=time_limit,
expires_at=expires_at, expires_at=expires_at,
for_group=for_group, for_group=for_group,
questions=questions, questions=questions,
@@ -24,6 +24,7 @@ class TestUpdateFields(TypedDict, total=False):
password: str | None password: str | None
expires_at: datetime | None expires_at: datetime | None
attempts: int | None attempts: int | None
time_limit: int | None
is_active: bool is_active: bool
are_results_viewable: bool are_results_viewable: bool
@@ -64,6 +65,7 @@ class TestDAO:
password: str | None = None, password: str | None = None,
expires_at: datetime | None = None, expires_at: datetime | None = None,
attempts: int | None = None, attempts: int | None = None,
time_limit: int | None = None,
is_active: bool = True, is_active: bool = True,
are_results_viewable: bool = False, are_results_viewable: bool = False,
) -> DomainTest: ) -> DomainTest:
@@ -74,6 +76,7 @@ class TestDAO:
password=password, password=password,
expires_at=expires_at, expires_at=expires_at,
attempts=attempts, attempts=attempts,
time_limit=time_limit,
is_active=is_active, is_active=is_active,
are_results_viewable=are_results_viewable, are_results_viewable=are_results_viewable,
) )
@@ -91,6 +94,7 @@ class TestDAO:
password: str | None | _UNSET = UNSET, password: str | None | _UNSET = UNSET,
expires_at: datetime | None | _UNSET = UNSET, expires_at: datetime | None | _UNSET = UNSET,
attempts: int | None | _UNSET = UNSET, attempts: int | None | _UNSET = UNSET,
time_limit: int | None | _UNSET = UNSET,
is_active: bool | _UNSET = UNSET, is_active: bool | _UNSET = UNSET,
are_results_viewable: bool | _UNSET = UNSET, are_results_viewable: bool | _UNSET = UNSET,
) -> DomainTest | None: ) -> DomainTest | None:
@@ -113,6 +117,8 @@ class TestDAO:
test.expires_at = expires_at test.expires_at = expires_at
if not isinstance(attempts, _UNSET): if not isinstance(attempts, _UNSET):
test.attempts = attempts test.attempts = attempts
if not isinstance(time_limit, _UNSET):
test.time_limit = time_limit
if not isinstance(is_active, _UNSET): if not isinstance(is_active, _UNSET):
test.is_active = is_active test.is_active = is_active
if not isinstance(are_results_viewable, _UNSET): if not isinstance(are_results_viewable, _UNSET):
@@ -15,6 +15,7 @@ class TestDTO:
password=self.model.password, password=self.model.password,
expires_at=self.model.expires_at, expires_at=self.model.expires_at,
attempts=self.model.attempts, attempts=self.model.attempts,
time_limit=self.model.time_limit,
is_active=self.model.is_active, is_active=self.model.is_active,
are_results_viewable=self.model.are_results_viewable, are_results_viewable=self.model.are_results_viewable,
created_at=self.model.created_at, created_at=self.model.created_at,
@@ -52,6 +52,7 @@ class Test(Base):
password: Mapped[str | None] = mapped_column(String(255), default=None) password: Mapped[str | None] = mapped_column(String(255), default=None)
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)
time_limit: 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) 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())