mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 18:35:28 +03:00
commit
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime, time
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
||||||
@@ -22,6 +22,7 @@ from trudex.infrastructure.database.repo.test_attempt import TestAttemptReposito
|
|||||||
from trudex.infrastructure.utils.config import Config
|
from trudex.infrastructure.utils.config import Config
|
||||||
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes
|
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes
|
||||||
from trudex.infrastructure.utils.test_id_to_hash import encode_id
|
from trudex.infrastructure.utils.test_id_to_hash import encode_id
|
||||||
|
from trudex.infrastructure.utils.timezone import MSK_TZ, to_msk
|
||||||
|
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
@@ -66,7 +67,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 "♾️ Без ограничений"
|
||||||
expires_str = f"📅 {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 "👥 Для всех"
|
||||||
|
|
||||||
test_info = (
|
test_info = (
|
||||||
@@ -79,7 +80,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
|||||||
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\n"
|
||||||
f"<b>Создан:</b> {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 "🟢 Активировать"
|
||||||
@@ -131,7 +132,8 @@ async def get_statistics_data(
|
|||||||
results = []
|
results = []
|
||||||
for attempt, user_name in attempts_with_users:
|
for attempt, user_name in attempts_with_users:
|
||||||
status = "✅" if attempt.is_passed else "❌"
|
status = "✅" if attempt.is_passed else "❌"
|
||||||
date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else ""
|
finished_at_msk = to_msk(attempt.finished_at)
|
||||||
|
date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else ""
|
||||||
results.append((f"{status} {user_name} — {attempt.score}% ({date_str})", attempt.id))
|
results.append((f"{status} {user_name} — {attempt.score}% ({date_str})", attempt.id))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -167,7 +169,8 @@ async def get_attempt_detail(
|
|||||||
return {"attempt_info": "❌ Результат не найден"}
|
return {"attempt_info": "❌ Результат не найден"}
|
||||||
|
|
||||||
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
||||||
date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else "—"
|
finished_at_msk = to_msk(attempt.finished_at)
|
||||||
|
date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else "—"
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
f"<b>📊 Результат прохождения</b>\n",
|
f"<b>📊 Результат прохождения</b>\n",
|
||||||
@@ -376,7 +379,7 @@ async def on_date_selected_for_test(_callback, _widget, manager: DialogManager,
|
|||||||
await _callback.answer("❌ Тест не найден")
|
await _callback.answer("❌ Тест не найден")
|
||||||
return
|
return
|
||||||
|
|
||||||
expires_at = datetime.combine(selected_date, datetime.min.time())
|
expires_at = datetime.combine(selected_date, time.min, tzinfo=MSK_TZ)
|
||||||
await test_dao.update(test_id, expires_at=expires_at)
|
await test_dao.update(test_id, expires_at=expires_at)
|
||||||
await _callback.answer("✅ Срок действия обновлен")
|
await _callback.answer("✅ Срок действия обновлен")
|
||||||
await manager.switch_to(AdminTestsSG.test_detail)
|
await manager.switch_to(AdminTestsSG.test_detail)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime, time
|
||||||
|
|
||||||
from aiogram.types import CallbackQuery, ContentType, Message
|
from aiogram.types import CallbackQuery, ContentType, Message
|
||||||
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
||||||
@@ -16,6 +16,7 @@ from trudex.infrastructure.database.dao.option import OptionDAO
|
|||||||
from trudex.infrastructure.database.dao.question import QuestionDAO
|
from trudex.infrastructure.database.dao.question import QuestionDAO
|
||||||
from trudex.infrastructure.database.dao.test import TestDAO
|
from trudex.infrastructure.database.dao.test import TestDAO
|
||||||
from trudex.infrastructure.database.repo.test import TestRepository
|
from trudex.infrastructure.database.repo.test import TestRepository
|
||||||
|
from trudex.infrastructure.utils.timezone import MSK_TZ, to_msk
|
||||||
|
|
||||||
|
|
||||||
async def on_title_input(message: Message, _widget: MessageInput, manager: DialogManager):
|
async def on_title_input(message: Message, _widget: MessageInput, manager: DialogManager):
|
||||||
@@ -110,7 +111,7 @@ async def on_skip_attempts(_callback: CallbackQuery, _button: Button, manager: D
|
|||||||
|
|
||||||
|
|
||||||
async def on_date_selected(_callback, _widget, manager: DialogManager, selected_date: date):
|
async def on_date_selected(_callback, _widget, manager: DialogManager, selected_date: date):
|
||||||
manager.dialog_data["expires_at"] = datetime.combine(selected_date, datetime.min.time())
|
manager.dialog_data["expires_at"] = datetime.combine(selected_date, time.min, tzinfo=MSK_TZ)
|
||||||
await manager.switch_to(CreateTestSG.input_for_group)
|
await manager.switch_to(CreateTestSG.input_for_group)
|
||||||
|
|
||||||
|
|
||||||
@@ -148,7 +149,8 @@ async def get_test_info(dialog_manager: DialogManager, **_kwargs):
|
|||||||
|
|
||||||
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 "♾️ Без ограничений"
|
||||||
expires_str = expires_at.strftime("%d.%m.%Y") if expires_at 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 "Для всех"
|
group_str = str(for_group) if for_group else "Для всех"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime, time
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.enums import ContentType
|
from aiogram.enums import ContentType
|
||||||
@@ -25,6 +25,7 @@ from trudex.infrastructure.database.repo.test_attempt import TestAttemptReposito
|
|||||||
from trudex.infrastructure.utils.config import Config
|
from trudex.infrastructure.utils.config import Config
|
||||||
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes
|
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes
|
||||||
from trudex.infrastructure.utils.test_id_to_hash import encode_id
|
from trudex.infrastructure.utils.test_id_to_hash import encode_id
|
||||||
|
from trudex.infrastructure.utils.timezone import MSK_TZ, to_msk
|
||||||
|
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
@@ -69,7 +70,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 "♾️ Без ограничений"
|
||||||
expires_str = f"📅 {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 "👥 Для всех"
|
||||||
|
|
||||||
test_info = (
|
test_info = (
|
||||||
@@ -82,7 +83,7 @@ async def get_test_detail(test_dao: FromDishka[TestDAO], test_repo: FromDishka[T
|
|||||||
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\n"
|
||||||
f"<b>Создан:</b> {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 "🟢 Активировать"
|
||||||
@@ -134,7 +135,8 @@ async def get_statistics_data(
|
|||||||
results = []
|
results = []
|
||||||
for attempt, user_name in attempts_with_users:
|
for attempt, user_name in attempts_with_users:
|
||||||
status = "✅" if attempt.is_passed else "❌"
|
status = "✅" if attempt.is_passed else "❌"
|
||||||
date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else ""
|
finished_at_msk = to_msk(attempt.finished_at)
|
||||||
|
date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else ""
|
||||||
results.append((f"{status} {user_name} — {attempt.score}% ({date_str})", attempt.id))
|
results.append((f"{status} {user_name} — {attempt.score}% ({date_str})", attempt.id))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -170,7 +172,8 @@ async def get_attempt_detail(
|
|||||||
return {"attempt_info": "❌ Результат не найден"}
|
return {"attempt_info": "❌ Результат не найден"}
|
||||||
|
|
||||||
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
||||||
date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else "—"
|
finished_at_msk = to_msk(attempt.finished_at)
|
||||||
|
date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else "—"
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
f"<b>📊 Результат прохождения</b>\n",
|
f"<b>📊 Результат прохождения</b>\n",
|
||||||
@@ -380,7 +383,7 @@ async def on_date_selected_for_test(_callback, _widget, manager: DialogManager,
|
|||||||
await _callback.answer("❌ Тест не найден")
|
await _callback.answer("❌ Тест не найден")
|
||||||
return
|
return
|
||||||
|
|
||||||
expires_at = datetime.combine(selected_date, datetime.min.time())
|
expires_at = datetime.combine(selected_date, time.min, tzinfo=MSK_TZ)
|
||||||
await test_dao.update(test_id, expires_at=expires_at)
|
await test_dao.update(test_id, expires_at=expires_at)
|
||||||
await _callback.answer("✅ Срок действия обновлен")
|
await _callback.answer("✅ Срок действия обновлен")
|
||||||
await manager.switch_to(CreatorTestsSG.test_detail)
|
await manager.switch_to(CreatorTestsSG.test_detail)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
from aiogram.filters import Command, CommandStart, CommandObject
|
from aiogram.filters import Command, CommandStart, CommandObject
|
||||||
from aiogram.types import ErrorEvent, Message
|
from aiogram.types import ErrorEvent, Message
|
||||||
@@ -19,6 +17,7 @@ from trudex.infrastructure.database.dao.test import TestDAO
|
|||||||
from trudex.infrastructure.database.dao.user import UserDAO
|
from trudex.infrastructure.database.dao.user import UserDAO
|
||||||
from trudex.infrastructure.utils.config import Config
|
from trudex.infrastructure.utils.config import Config
|
||||||
from trudex.infrastructure.utils.test_id_to_hash import decode_id
|
from trudex.infrastructure.utils.test_id_to_hash import decode_id
|
||||||
|
from trudex.infrastructure.utils.timezone import now_msk
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ async def validate_deeplink_test(
|
|||||||
if not test.is_active:
|
if not test.is_active:
|
||||||
return False, "❌ Тест деактивирован"
|
return False, "❌ Тест деактивирован"
|
||||||
|
|
||||||
if test.expires_at and test.expires_at < datetime.now(timezone.utc):
|
if test.expires_at and test.expires_at < now_msk():
|
||||||
return False, "❌ Срок действия теста истек"
|
return False, "❌ Срок действия теста истек"
|
||||||
|
|
||||||
user = await user_dao.get_by_id(user_id)
|
user = await user_dao.get_by_id(user_id)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import timedelta
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
||||||
@@ -20,6 +20,25 @@ from trudex.infrastructure.database.repo.test_attempt import TestAttemptReposito
|
|||||||
from trudex.infrastructure.utils.config import Config
|
from trudex.infrastructure.utils.config import Config
|
||||||
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes
|
from trudex.infrastructure.utils.qr_generator import generate_qr_bytes
|
||||||
from trudex.infrastructure.utils.test_id_to_hash import encode_id
|
from trudex.infrastructure.utils.test_id_to_hash import encode_id
|
||||||
|
from trudex.infrastructure.utils.timezone import now_msk, to_msk
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def can_edit_field(updated_at: datetime | None) -> bool:
|
||||||
|
if updated_at is None:
|
||||||
|
return True
|
||||||
|
updated_at_msk = to_msk(updated_at)
|
||||||
|
assert updated_at_msk is not None
|
||||||
|
return now_msk() - updated_at_msk >= timedelta(hours=24)
|
||||||
|
|
||||||
|
|
||||||
|
def get_remaining_time(updated_at: datetime) -> str:
|
||||||
|
updated_at_msk = to_msk(updated_at)
|
||||||
|
assert updated_at_msk is not None
|
||||||
|
remaining = timedelta(hours=24) - (now_msk() - updated_at_msk)
|
||||||
|
hours = int(remaining.total_seconds() // 3600)
|
||||||
|
minutes = int((remaining.total_seconds() % 3600) // 60)
|
||||||
|
return f"{hours}ч {minutes}м"
|
||||||
|
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
@@ -57,19 +76,6 @@ async def get_user_data(
|
|||||||
return {"user_info": user_info}
|
return {"user_info": user_info}
|
||||||
|
|
||||||
|
|
||||||
def can_edit_field(updated_at: datetime | None) -> bool:
|
|
||||||
if updated_at is None:
|
|
||||||
return True
|
|
||||||
return datetime.now(timezone.utc) - updated_at >= timedelta(hours=24)
|
|
||||||
|
|
||||||
|
|
||||||
def get_remaining_time(updated_at: datetime) -> str:
|
|
||||||
remaining = timedelta(hours=24) - (datetime.now(timezone.utc) - updated_at)
|
|
||||||
hours = int(remaining.total_seconds() // 3600)
|
|
||||||
minutes = int((remaining.total_seconds() % 3600) // 60)
|
|
||||||
return f"{hours}ч {minutes}м"
|
|
||||||
|
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
async def on_edit_name_clicked(
|
async def on_edit_name_clicked(
|
||||||
_callback: CallbackQuery,
|
_callback: CallbackQuery,
|
||||||
@@ -79,6 +85,7 @@ async def on_edit_name_clicked(
|
|||||||
):
|
):
|
||||||
assert _callback.from_user is not None
|
assert _callback.from_user is not None
|
||||||
user = await user_dao.get_by_id(_callback.from_user.id)
|
user = await user_dao.get_by_id(_callback.from_user.id)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
await _callback.answer("❌ Пользователь не найден")
|
await _callback.answer("❌ Пользователь не найден")
|
||||||
return
|
return
|
||||||
@@ -101,6 +108,7 @@ async def on_edit_group_clicked(
|
|||||||
):
|
):
|
||||||
assert _callback.from_user is not None
|
assert _callback.from_user is not None
|
||||||
user = await user_dao.get_by_id(_callback.from_user.id)
|
user = await user_dao.get_by_id(_callback.from_user.id)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
await _callback.answer("❌ Пользователь не найден")
|
await _callback.answer("❌ Пользователь не найден")
|
||||||
return
|
return
|
||||||
@@ -139,8 +147,15 @@ async def on_name_input(
|
|||||||
return
|
return
|
||||||
|
|
||||||
name = message.text.strip()[:128]
|
name = message.text.strip()[:128]
|
||||||
await user_dao.update(message.from_user.id, name=name, name_updated_at=datetime.now(timezone.utc))
|
result = await user_dao.update(
|
||||||
|
user_id=message.from_user.id,
|
||||||
|
name=name,
|
||||||
|
name_updated_at=now_msk(),
|
||||||
|
)
|
||||||
|
if result:
|
||||||
await message.answer("✅ Имя обновлено")
|
await message.answer("✅ Имя обновлено")
|
||||||
|
else:
|
||||||
|
await message.answer("❌ Не удалось обновить имя")
|
||||||
await manager.switch_to(UserMenuSG.main)
|
await manager.switch_to(UserMenuSG.main)
|
||||||
|
|
||||||
|
|
||||||
@@ -159,8 +174,15 @@ async def on_group_selected(
|
|||||||
user_dao: FromDishka[UserDAO],
|
user_dao: FromDishka[UserDAO],
|
||||||
):
|
):
|
||||||
assert _callback.from_user is not None
|
assert _callback.from_user is not None
|
||||||
await user_dao.update(_callback.from_user.id, group=int(item_id), group_updated_at=datetime.now(timezone.utc))
|
result = await user_dao.update(
|
||||||
|
user_id=_callback.from_user.id,
|
||||||
|
group=int(item_id),
|
||||||
|
group_updated_at=now_msk(),
|
||||||
|
)
|
||||||
|
if result:
|
||||||
await _callback.answer("✅ Группа обновлена")
|
await _callback.answer("✅ Группа обновлена")
|
||||||
|
else:
|
||||||
|
await _callback.answer("❌ Не удалось обновить группу")
|
||||||
await manager.switch_to(UserMenuSG.main)
|
await manager.switch_to(UserMenuSG.main)
|
||||||
|
|
||||||
|
|
||||||
@@ -255,9 +277,11 @@ async def get_test_detail(
|
|||||||
attempts = await attempt_repo.get_user_test_attempts(user_id, test_id)
|
attempts = await attempt_repo.get_user_test_attempts(user_id, test_id)
|
||||||
finished_attempts = [a for a in attempts if a.finished_at]
|
finished_attempts = [a for a in attempts if a.finished_at]
|
||||||
|
|
||||||
password_str = f"🔒 Требуется пароль" 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)}/♾️"
|
||||||
expires_str = f"📅 До {test.expires_at.strftime('%d.%m.%Y %H:%M')}" if test.expires_at 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 "📅 Без срока"
|
||||||
group_str = f"🎓 Для группы {test.for_group}" if test.for_group else "👥 Для всех"
|
group_str = f"🎓 Для группы {test.for_group}" if test.for_group else "👥 Для всех"
|
||||||
|
|
||||||
test_info = (
|
test_info = (
|
||||||
@@ -286,7 +310,8 @@ async def get_my_results(
|
|||||||
results = []
|
results = []
|
||||||
for attempt, test_title in attempts_with_tests:
|
for attempt, test_title in attempts_with_tests:
|
||||||
status = "✅" if attempt.is_passed else "❌"
|
status = "✅" if attempt.is_passed else "❌"
|
||||||
date_str = attempt.finished_at.strftime("%d.%m.%Y") if attempt.finished_at else ""
|
finished_at_msk = to_msk(attempt.finished_at)
|
||||||
|
date_str = finished_at_msk.strftime("%d.%m.%Y") if finished_at_msk else ""
|
||||||
results.append((f"{status} {test_title} — {attempt.score}% ({date_str})", attempt.id))
|
results.append((f"{status} {test_title} — {attempt.score}% ({date_str})", attempt.id))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -325,7 +350,8 @@ async def get_result_detail(
|
|||||||
test_title = test.title if test else "Неизвестный тест"
|
test_title = test.title if test else "Неизвестный тест"
|
||||||
|
|
||||||
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
status = "✅ Пройден" if attempt.is_passed else "❌ Не пройден"
|
||||||
date_str = attempt.finished_at.strftime("%d.%m.%Y %H:%M") if attempt.finished_at else "—"
|
finished_at_msk = to_msk(attempt.finished_at)
|
||||||
|
date_str = finished_at_msk.strftime("%d.%m.%Y %H:%M") if finished_at_msk else "—"
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
f"<b>📝 {test_title}</b>\n",
|
f"<b>📝 {test_title}</b>\n",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
from aiogram_dialog import Dialog, DialogManager, StartMode, Window
|
||||||
from aiogram_dialog.widgets.input import MessageInput
|
from aiogram_dialog.widgets.input import MessageInput
|
||||||
@@ -14,6 +12,7 @@ from trudex.infrastructure.database.dao.user_answer import UserAnswerDAO
|
|||||||
from trudex.infrastructure.database.models import QuestionType
|
from trudex.infrastructure.database.models import QuestionType
|
||||||
from trudex.infrastructure.database.repo.test import TestRepository
|
from trudex.infrastructure.database.repo.test import TestRepository
|
||||||
from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
from trudex.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
||||||
|
from trudex.infrastructure.utils.timezone import now_msk
|
||||||
|
|
||||||
|
|
||||||
async def get_state_for_question_type(question_type: str):
|
async def get_state_for_question_type(question_type: str):
|
||||||
@@ -51,7 +50,7 @@ async def on_start_test(
|
|||||||
await _callback.answer("❌ Тест деактивирован")
|
await _callback.answer("❌ Тест деактивирован")
|
||||||
return
|
return
|
||||||
|
|
||||||
if test.expires_at and test.expires_at < datetime.now(timezone.utc):
|
if test.expires_at and test.expires_at < now_msk():
|
||||||
await _callback.answer("❌ Срок действия теста истек")
|
await _callback.answer("❌ Срок действия теста истек")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class UserDAO:
|
|||||||
last_name: str | None = None,
|
last_name: str | None = None,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
group: int | None = None,
|
group: int | None = None,
|
||||||
is_admin: bool = False,
|
is_admin: bool | None = None,
|
||||||
) -> DomainUser:
|
) -> DomainUser:
|
||||||
result = await self.session.execute(
|
result = await self.session.execute(
|
||||||
select(User).where(User.id == user_id)
|
select(User).where(User.id == user_id)
|
||||||
@@ -139,5 +139,5 @@ class UserDAO:
|
|||||||
last_name=last_name,
|
last_name=last_name,
|
||||||
name=name,
|
name=name,
|
||||||
group=group,
|
group=group,
|
||||||
is_admin=is_admin,
|
is_admin=is_admin or False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from datetime import datetime
|
|
||||||
from typing import final
|
from typing import final
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
@@ -13,6 +12,7 @@ from trudex.infrastructure.database.dto.user_answer import UserAnswerDTO
|
|||||||
from trudex.infrastructure.database.models import \
|
from trudex.infrastructure.database.models import \
|
||||||
TestAttempt as TestAttemptModel
|
TestAttempt as TestAttemptModel
|
||||||
from trudex.infrastructure.database.models import UserAnswer as UserAnswerModel
|
from trudex.infrastructure.database.models import UserAnswer as UserAnswerModel
|
||||||
|
from trudex.infrastructure.utils.timezone import now_msk
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@@ -132,7 +132,7 @@ class TestAttemptRepository:
|
|||||||
async def finish_attempt(self, attempt_id: int, score: int, is_passed: bool) -> TestAttempt | None:
|
async def finish_attempt(self, attempt_id: int, score: int, is_passed: bool) -> TestAttempt | None:
|
||||||
return await self.attempt_dao.update(
|
return await self.attempt_dao.update(
|
||||||
attempt_id=attempt_id,
|
attempt_id=attempt_id,
|
||||||
finished_at=datetime.utcnow(),
|
finished_at=now_msk(),
|
||||||
score=score,
|
score=score,
|
||||||
is_passed=is_passed
|
is_passed=is_passed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from dishka import AsyncContainer
|
from dishka import AsyncContainer
|
||||||
|
|
||||||
from trudex.infrastructure.database.dao.test import TestDAO
|
from trudex.infrastructure.database.dao.test import TestDAO
|
||||||
from trudex.infrastructure.database.models import Test
|
from trudex.infrastructure.utils.timezone import now_msk
|
||||||
|
|
||||||
|
|
||||||
async def deactivate_expired_tests(container: AsyncContainer):
|
async def deactivate_expired_tests(container: AsyncContainer):
|
||||||
@@ -13,5 +11,5 @@ async def deactivate_expired_tests(container: AsyncContainer):
|
|||||||
tests = await test_dao.get_all()
|
tests = await test_dao.get_all()
|
||||||
|
|
||||||
for test in tests:
|
for test in tests:
|
||||||
if test.expires_at and test.expires_at < datetime.utcnow() and test.is_active:
|
if test.expires_at and test.expires_at < now_msk() and test.is_active:
|
||||||
await test_dao.update(test.id, is_active=False)
|
await test_dao.update(test.id, is_active=False)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
MSK_TZ = ZoneInfo("Europe/Moscow")
|
||||||
|
|
||||||
|
|
||||||
|
def now_msk() -> datetime:
|
||||||
|
return datetime.now(MSK_TZ)
|
||||||
|
|
||||||
|
|
||||||
|
def to_msk(dt: datetime | None) -> datetime | None:
|
||||||
|
if dt is None:
|
||||||
|
return None
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
return dt.replace(tzinfo=MSK_TZ)
|
||||||
|
return dt.astimezone(MSK_TZ)
|
||||||
Reference in New Issue
Block a user