mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 10:25:28 +03:00
update
This commit is contained in:
@@ -100,11 +100,41 @@ async def on_attempts_input(message: Message, _widget: MessageInput, manager: Di
|
||||
return
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
@@ -139,11 +169,13 @@ async def get_test_info(dialog_manager: DialogManager, **_kwargs):
|
||||
description = dialog_manager.dialog_data.get("description", "—")
|
||||
password = dialog_manager.dialog_data.get("password")
|
||||
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")
|
||||
for_group = dialog_manager.dialog_data.get("for_group")
|
||||
|
||||
password_str = f"🔒 {password}" if password 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_str = expires_at_msk.strftime("%d.%m.%Y") if expires_at_msk 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> {password_str}\n"
|
||||
f"<b>Попыток:</b> {attempts_str}\n"
|
||||
f"<b>Время:</b> {time_limit_str}\n"
|
||||
f"<b>Истекает:</b> {expires_str}\n"
|
||||
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")
|
||||
password = manager.dialog_data.get("password")
|
||||
attempts = manager.dialog_data.get("attempts")
|
||||
time_limit = manager.dialog_data.get("time_limit")
|
||||
expires_at = manager.dialog_data.get("expires_at")
|
||||
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,
|
||||
password=password,
|
||||
attempts=attempts,
|
||||
time_limit=time_limit,
|
||||
expires_at=expires_at,
|
||||
for_group=for_group,
|
||||
)
|
||||
@@ -470,6 +505,12 @@ shared_create_test_dialog = Dialog(
|
||||
Button(Const("⏭️ Без ограничений"), id="skip_attempts", on_click=on_skip_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(
|
||||
Const("<b>📅 Срок действия</b>\n\n🗓 <b>Выберите дату истечения теста</b> или пропустите:"),
|
||||
Calendar(id="calendar", on_click=on_date_selected),
|
||||
|
||||
@@ -15,6 +15,7 @@ class SharedTestsSG(StatesGroup):
|
||||
edit_menu = State()
|
||||
edit_password = State()
|
||||
edit_attempts = State()
|
||||
edit_time_limit = State()
|
||||
edit_group = State()
|
||||
edit_expires = State()
|
||||
statistics = State()
|
||||
@@ -38,6 +39,7 @@ class SharedCreateTestSG(StatesGroup):
|
||||
input_description = State()
|
||||
input_password = State()
|
||||
input_attempts = State()
|
||||
input_time_limit = State()
|
||||
input_expires_at = State()
|
||||
input_for_group = State()
|
||||
confirm_test_info = State()
|
||||
|
||||
@@ -36,6 +36,7 @@ SPEC_INFO = """<b>📋 Спецификация формата JSON</b>
|
||||
"description": "Описание теста",
|
||||
"password": null,
|
||||
"attempts": null,
|
||||
"time_limit": null,
|
||||
"expires_at": null,
|
||||
"for_group": null,
|
||||
"questions": [...]
|
||||
@@ -46,6 +47,7 @@ SPEC_INFO = """<b>📋 Спецификация формата JSON</b>
|
||||
• <code>description</code> — описание (до 2000 символов)
|
||||
• <code>password</code> — пароль для доступа или <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>for_group</code> — номер группы или <code>null</code> для всех
|
||||
|
||||
@@ -90,6 +92,7 @@ TEMPLATE_ULTIMATE = """// ══════════════════
|
||||
// ⚙️ НАСТРОЙКИ:
|
||||
// • Пароль: test2024
|
||||
// • Попыток: 5
|
||||
// • Лимит времени: 1800 секунд (30 минут)
|
||||
// • Срок действия: 31 декабря 2026, 23:59
|
||||
// • Для группы: 2024 (или null для всех)
|
||||
//
|
||||
@@ -104,6 +107,7 @@ TEMPLATE_ULTIMATE = """// ══════════════════
|
||||
// 💡 ПОДСКАЗКИ:
|
||||
// • null означает "не задано" / "без ограничений"
|
||||
// • expires_at в формате ISO 8601: YYYY-MM-DDTHH:MM:SS
|
||||
// • time_limit в секундах (1-86400), null для без ограничений
|
||||
// • for_group - номер группы или null для всех пользователей
|
||||
// • image_url - URL изображения к вопросу (опционально)
|
||||
//
|
||||
@@ -114,6 +118,7 @@ TEMPLATE_ULTIMATE = """// ══════════════════
|
||||
"description": "Полная демонстрация всех возможностей формата: разные типы вопросов, настройки доступа, ограничения по времени и группам",
|
||||
"password": "test2024",
|
||||
"attempts": 5,
|
||||
"time_limit": 1800,
|
||||
"expires_at": "2026-12-31T23:59:59",
|
||||
"for_group": 2024,
|
||||
"questions": [
|
||||
@@ -225,6 +230,7 @@ async def on_test_selected_for_export(
|
||||
"description": test.description,
|
||||
"password": test.password,
|
||||
"attempts": test.attempts,
|
||||
"time_limit": test.time_limit,
|
||||
"expires_at": test.expires_at.isoformat() if test.expires_at else None,
|
||||
"for_group": test.for_group,
|
||||
"questions": [],
|
||||
@@ -340,6 +346,7 @@ async def create_test_from_parsed(
|
||||
description=parsed.description,
|
||||
password=parsed.password,
|
||||
attempts=parsed.attempts,
|
||||
time_limit=parsed.time_limit,
|
||||
expires_at=parsed.expires_at,
|
||||
for_group=parsed.for_group,
|
||||
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 "🔴 Деактивирован"
|
||||
password_str = f"🔒 {test.password}" if test.password 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 "📅 Без срока"
|
||||
group_str = f"🎓 Группа {test.for_group}" if test.for_group 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> {password_str}\n"
|
||||
f"<b>Попытки:</b> {attempts_str}\n"
|
||||
f"<b>Время:</b> {time_limit_str}\n"
|
||||
f"<b>Срок:</b> {expires_str}\n"
|
||||
f"<b>Группа:</b> {group_str}\n"
|
||||
f"<b>Видимость:</b> {results_str}\n\n"
|
||||
@@ -254,6 +256,7 @@ async def on_export_test(
|
||||
"description": test.description,
|
||||
"password": test.password,
|
||||
"attempts": test.attempts,
|
||||
"time_limit": test.time_limit,
|
||||
"expires_at": test.expires_at.isoformat() if test.expires_at else None,
|
||||
"for_group": test.for_group,
|
||||
"questions": [],
|
||||
@@ -361,6 +364,10 @@ async def on_edit_attempts(_callback: CallbackQuery, _button: Button, manager: D
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
async def get_groups_for_edit(dialog_manager: DialogManager, group_dao: FromDishka[GroupDAO], **_kwargs):
|
||||
groups = await group_dao.get_all()
|
||||
@@ -561,6 +613,7 @@ shared_tests_dialog = Dialog(
|
||||
Column(
|
||||
Button(Const("🔑 Пароль"), id="edit_password", on_click=on_edit_password),
|
||||
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_expires", on_click=on_edit_expires),
|
||||
Button(Const("◀️ Назад"), id="back", on_click=on_back_to_detail),
|
||||
@@ -585,6 +638,15 @@ shared_tests_dialog = Dialog(
|
||||
),
|
||||
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(
|
||||
Const("<b>👥 Изменение группы</b>\n\n🎓 <b>Выберите группу</b> или удалите привязку:"),
|
||||
ScrollingGroup(
|
||||
|
||||
@@ -41,13 +41,15 @@ async def get_deeplink_test_data(
|
||||
|
||||
password_str = "🔒 Требуется пароль" if test.password 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 = (
|
||||
f"<b>📝 {test.title}</b>\n\n"
|
||||
f"<blockquote>{test.description or '—'}</blockquote>\n\n"
|
||||
f"<b>Вопросов:</b> {questions_count}\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)}
|
||||
|
||||
@@ -278,6 +278,7 @@ async def get_test_detail(
|
||||
|
||||
password_str = "🔒 Требуется пароль" if test.password else "🔓 Без пароля"
|
||||
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_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"{password_str}\n"
|
||||
f"{attempts_str}\n"
|
||||
f"{time_limit_str}\n"
|
||||
f"{expires_str}\n"
|
||||
f"{group_str}"
|
||||
)
|
||||
|
||||
@@ -41,6 +41,7 @@ class Test:
|
||||
password: str | None = None
|
||||
expires_at: datetime | None = None
|
||||
attempts: int | None = None
|
||||
time_limit: int | None = None
|
||||
is_active: bool = True
|
||||
are_results_viewable: bool = False
|
||||
created_at: datetime | None = None
|
||||
|
||||
@@ -24,6 +24,7 @@ class ParsedTest:
|
||||
description: str | None
|
||||
password: str | None
|
||||
attempts: int | None
|
||||
time_limit: int | None
|
||||
expires_at: datetime | None
|
||||
for_group: int | None
|
||||
questions: list[ParsedQuestion]
|
||||
@@ -53,6 +54,7 @@ class TestParser:
|
||||
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)
|
||||
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)
|
||||
for_group = self._parse_int(data, "for_group", required=False, errors=errors)
|
||||
|
||||
@@ -68,6 +70,7 @@ class TestParser:
|
||||
description=description,
|
||||
password=password,
|
||||
attempts=attempts,
|
||||
time_limit=time_limit,
|
||||
expires_at=expires_at,
|
||||
for_group=for_group,
|
||||
questions=questions,
|
||||
|
||||
@@ -24,6 +24,7 @@ class TestUpdateFields(TypedDict, total=False):
|
||||
password: str | None
|
||||
expires_at: datetime | None
|
||||
attempts: int | None
|
||||
time_limit: int | None
|
||||
is_active: bool
|
||||
are_results_viewable: bool
|
||||
|
||||
@@ -64,6 +65,7 @@ class TestDAO:
|
||||
password: str | None = None,
|
||||
expires_at: datetime | None = None,
|
||||
attempts: int | None = None,
|
||||
time_limit: int | None = None,
|
||||
is_active: bool = True,
|
||||
are_results_viewable: bool = False,
|
||||
) -> DomainTest:
|
||||
@@ -74,6 +76,7 @@ class TestDAO:
|
||||
password=password,
|
||||
expires_at=expires_at,
|
||||
attempts=attempts,
|
||||
time_limit=time_limit,
|
||||
is_active=is_active,
|
||||
are_results_viewable=are_results_viewable,
|
||||
)
|
||||
@@ -91,6 +94,7 @@ class TestDAO:
|
||||
password: str | None | _UNSET = UNSET,
|
||||
expires_at: datetime | None | _UNSET = UNSET,
|
||||
attempts: int | None | _UNSET = UNSET,
|
||||
time_limit: int | None | _UNSET = UNSET,
|
||||
is_active: bool | _UNSET = UNSET,
|
||||
are_results_viewable: bool | _UNSET = UNSET,
|
||||
) -> DomainTest | None:
|
||||
@@ -113,6 +117,8 @@ class TestDAO:
|
||||
test.expires_at = expires_at
|
||||
if not isinstance(attempts, _UNSET):
|
||||
test.attempts = attempts
|
||||
if not isinstance(time_limit, _UNSET):
|
||||
test.time_limit = time_limit
|
||||
if not isinstance(is_active, _UNSET):
|
||||
test.is_active = is_active
|
||||
if not isinstance(are_results_viewable, _UNSET):
|
||||
|
||||
@@ -15,6 +15,7 @@ class TestDTO:
|
||||
password=self.model.password,
|
||||
expires_at=self.model.expires_at,
|
||||
attempts=self.model.attempts,
|
||||
time_limit=self.model.time_limit,
|
||||
is_active=self.model.is_active,
|
||||
are_results_viewable=self.model.are_results_viewable,
|
||||
created_at=self.model.created_at,
|
||||
|
||||
@@ -52,6 +52,7 @@ class Test(Base):
|
||||
password: Mapped[str | None] = mapped_column(String(255), default=None)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(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)
|
||||
are_results_viewable: Mapped[bool] = mapped_column(default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
|
||||
|
||||
Reference in New Issue
Block a user