This commit is contained in:
2026-01-04 01:01:07 +03:00
parent c80e8c6935
commit 53b846009b
10 changed files with 97 additions and 51 deletions
@@ -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)
+2 -3
View File
@@ -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)