mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 02:15:29 +03:00
update
This commit is contained in:
+4
-5
@@ -59,11 +59,10 @@ def do_run_migrations(connection: Connection):
|
|||||||
|
|
||||||
|
|
||||||
async def run_async_migrations() -> None:
|
async def run_async_migrations() -> None:
|
||||||
"""In this scenario we need to create an Engine
|
connectable = create_async_engine(
|
||||||
and associate a connection with the context.
|
db_config.url,
|
||||||
|
connect_args={"server_settings": {"timezone": "UTC"}},
|
||||||
"""
|
)
|
||||||
connectable = create_async_engine(db_config.url)
|
|
||||||
|
|
||||||
async with connectable.connect() as connection:
|
async with connectable.connect() as connection:
|
||||||
await connection.run_sync(do_run_migrations)
|
await connection.run_sync(do_run_migrations)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""set_timezone_to_utc
|
||||||
|
|
||||||
|
Revision ID: 2ba7282de7d9
|
||||||
|
Revises: c4d5e6f7a8b9
|
||||||
|
Create Date: 2026-02-20 01:14:15.088425
|
||||||
|
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = '2ba7282de7d9'
|
||||||
|
down_revision: str | None = 'c4d5e6f7a8b9'
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute("SET timezone = 'UTC'")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time, timezone
|
||||||
|
|
||||||
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
|
||||||
@@ -139,7 +139,8 @@ async def on_skip_time_limit(_callback: CallbackQuery, _button: Button, manager:
|
|||||||
|
|
||||||
|
|
||||||
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, time.min)
|
expires_at = datetime.combine(selected_date, time.min, tzinfo=timezone.utc).replace(tzinfo=None)
|
||||||
|
manager.dialog_data["expires_at"] = expires_at
|
||||||
await manager.switch_to(SharedCreateTestSG.input_for_group)
|
await manager.switch_to(SharedCreateTestSG.input_for_group)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time, timezone
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
||||||
@@ -520,7 +520,7 @@ async def on_date_selected_for_test(
|
|||||||
await _callback.answer("❌ Тест не найден")
|
await _callback.answer("❌ Тест не найден")
|
||||||
return
|
return
|
||||||
|
|
||||||
expires_at = datetime.combine(selected_date, time.min)
|
expires_at = datetime.combine(selected_date, time.min, tzinfo=timezone.utc).replace(tzinfo=None)
|
||||||
result = await test_service.update_expires(test_id, expires_at)
|
result = await test_service.update_expires(test_id, expires_at)
|
||||||
await _callback.answer(result.message)
|
await _callback.answer(result.message)
|
||||||
await manager.switch_to(SharedTestsSG.test_detail)
|
await manager.switch_to(SharedTestsSG.test_detail)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from quizzi.infrastructure.database.models import QuestionType
|
|||||||
from quizzi.infrastructure.database.repo.test import TestRepository
|
from quizzi.infrastructure.database.repo.test import TestRepository
|
||||||
from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
||||||
from quizzi.infrastructure.utils.rate_limiter import PasswordRateLimiter
|
from quizzi.infrastructure.utils.rate_limiter import PasswordRateLimiter
|
||||||
from quizzi.infrastructure.utils.timezone import now_msk_naive, to_msk
|
from quizzi.infrastructure.utils.timezone import now_utc_naive, to_msk
|
||||||
|
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
@@ -137,7 +137,7 @@ async def start_test_without_password(
|
|||||||
return
|
return
|
||||||
|
|
||||||
attempt = await attempt_repo.attempt_dao.create(user_id=user_id, test_id=test_id)
|
attempt = await attempt_repo.attempt_dao.create(user_id=user_id, test_id=test_id)
|
||||||
started_at = now_msk_naive()
|
started_at = now_utc_naive()
|
||||||
|
|
||||||
first_question, _ = await test_repo.get_question_with_options(questions[0].id)
|
first_question, _ = await test_repo.get_question_with_options(questions[0].id)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from quizzi.infrastructure.database.repo.test_attempt import TestAttemptReposito
|
|||||||
from quizzi.infrastructure.utils.config import Config
|
from quizzi.infrastructure.utils.config import Config
|
||||||
from quizzi.infrastructure.utils.qr_generator import generate_qr_bytes
|
from quizzi.infrastructure.utils.qr_generator import generate_qr_bytes
|
||||||
from quizzi.infrastructure.utils.test_id_to_hash import encode_id
|
from quizzi.infrastructure.utils.test_id_to_hash import encode_id
|
||||||
from quizzi.infrastructure.utils.timezone import now_msk, now_msk_naive, to_msk
|
from quizzi.infrastructure.utils.timezone import now_utc, now_utc_naive, to_msk
|
||||||
|
|
||||||
|
|
||||||
def can_edit_field(updated_at: datetime | None) -> bool:
|
def can_edit_field(updated_at: datetime | None) -> bool:
|
||||||
@@ -28,13 +28,17 @@ def can_edit_field(updated_at: datetime | None) -> bool:
|
|||||||
return True
|
return True
|
||||||
updated_at_msk = to_msk(updated_at)
|
updated_at_msk = to_msk(updated_at)
|
||||||
assert updated_at_msk is not None
|
assert updated_at_msk is not None
|
||||||
return now_msk() - updated_at_msk >= timedelta(hours=24)
|
now_msk = to_msk(now_utc())
|
||||||
|
assert now_msk is not None
|
||||||
|
return now_msk - updated_at_msk >= timedelta(hours=24)
|
||||||
|
|
||||||
|
|
||||||
def get_remaining_time(updated_at: datetime) -> str:
|
def get_remaining_time(updated_at: datetime) -> str:
|
||||||
updated_at_msk = to_msk(updated_at)
|
updated_at_msk = to_msk(updated_at)
|
||||||
assert updated_at_msk is not None
|
assert updated_at_msk is not None
|
||||||
remaining = timedelta(hours=24) - (now_msk() - updated_at_msk)
|
now_msk = to_msk(now_utc())
|
||||||
|
assert now_msk is not None
|
||||||
|
remaining = timedelta(hours=24) - (now_msk - updated_at_msk)
|
||||||
hours = int(remaining.total_seconds() // 3600)
|
hours = int(remaining.total_seconds() // 3600)
|
||||||
minutes = int((remaining.total_seconds() % 3600) // 60)
|
minutes = int((remaining.total_seconds() % 3600) // 60)
|
||||||
return f"{hours}ч {minutes}м"
|
return f"{hours}ч {minutes}м"
|
||||||
@@ -149,7 +153,7 @@ async def on_name_input(
|
|||||||
result = await user_dao.update(
|
result = await user_dao.update(
|
||||||
user_id=message.from_user.id,
|
user_id=message.from_user.id,
|
||||||
name=name,
|
name=name,
|
||||||
name_updated_at=now_msk_naive(),
|
name_updated_at=now_utc_naive(),
|
||||||
)
|
)
|
||||||
if result:
|
if result:
|
||||||
await message.answer("✅ Имя обновлено")
|
await message.answer("✅ Имя обновлено")
|
||||||
@@ -176,7 +180,7 @@ async def on_group_selected(
|
|||||||
result = await user_dao.update(
|
result = await user_dao.update(
|
||||||
user_id=_callback.from_user.id,
|
user_id=_callback.from_user.id,
|
||||||
group=int(item_id),
|
group=int(item_id),
|
||||||
group_updated_at=now_msk_naive(),
|
group_updated_at=now_utc_naive(),
|
||||||
)
|
)
|
||||||
if result:
|
if result:
|
||||||
await _callback.answer("✅ Группа обновлена")
|
await _callback.answer("✅ Группа обновлена")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from dishka.integrations.aiogram_dialog import inject
|
|||||||
from quizzi.application.bot.user_dialogs.states import UserDeeplinkSG, UserMenuSG, UserRegistrationSG
|
from quizzi.application.bot.user_dialogs.states import UserDeeplinkSG, UserMenuSG, UserRegistrationSG
|
||||||
from quizzi.infrastructure.database.dao.group import GroupDAO
|
from quizzi.infrastructure.database.dao.group import GroupDAO
|
||||||
from quizzi.infrastructure.database.dao.user import UserDAO
|
from quizzi.infrastructure.database.dao.user import UserDAO
|
||||||
from quizzi.infrastructure.utils.timezone import now_msk_naive
|
from quizzi.infrastructure.utils.timezone import now_utc_naive
|
||||||
|
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
@@ -40,7 +40,7 @@ async def on_name_input(
|
|||||||
pending_test_id = start_data.get("pending_test_id")
|
pending_test_id = start_data.get("pending_test_id")
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
await user_dao.update(user_id=user_id, name=name, name_updated_at=now_msk_naive())
|
await user_dao.update(user_id=user_id, name=name, name_updated_at=now_utc_naive())
|
||||||
|
|
||||||
manager.dialog_data["name"] = name
|
manager.dialog_data["name"] = name
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ async def on_group_selected(
|
|||||||
pending_test_id = start_data.get("pending_test_id")
|
pending_test_id = start_data.get("pending_test_id")
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
await user_dao.update(user_id=user_id, group=int(item_id), group_updated_at=now_msk_naive())
|
await user_dao.update(user_id=user_id, group=int(item_id), group_updated_at=now_utc_naive())
|
||||||
|
|
||||||
if pending_test_id:
|
if pending_test_id:
|
||||||
await manager.start(
|
await manager.start(
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ async def on_start_test(
|
|||||||
await _callback.answer("❌ Тест деактивирован")
|
await _callback.answer("❌ Тест деактивирован")
|
||||||
return
|
return
|
||||||
|
|
||||||
if test.expires_at and test.expires_at < now_msk_naive():
|
if test.expires_at and test.expires_at < now_utc_naive():
|
||||||
await _callback.answer("❌ Срок действия теста истек")
|
await _callback.answer("❌ Срок действия теста истек")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ def new_session_maker(db_url: str) -> async_sessionmaker[AsyncSession]:
|
|||||||
max_overflow=15,
|
max_overflow=15,
|
||||||
connect_args={
|
connect_args={
|
||||||
"timeout": 5,
|
"timeout": 5,
|
||||||
|
"server_settings": {"timezone": "UTC"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return async_sessionmaker(engine, class_=AsyncSession, autoflush=False, expire_on_commit=False)
|
return async_sessionmaker(engine, class_=AsyncSession, autoflush=False, expire_on_commit=False)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -34,7 +34,7 @@ class TestAttemptDAO:
|
|||||||
attempt = TestAttempt(
|
attempt = TestAttempt(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
test_id=test_id,
|
test_id=test_id,
|
||||||
started_at=datetime.utcnow(),
|
started_at=datetime.now(timezone.utc).replace(tzinfo=None),
|
||||||
score=score,
|
score=score,
|
||||||
is_passed=is_passed,
|
is_passed=is_passed,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from quizzi.infrastructure.database.models import Test as TestModel
|
|||||||
from quizzi.infrastructure.database.models import TestAttempt as TestAttemptModel
|
from quizzi.infrastructure.database.models import TestAttempt as TestAttemptModel
|
||||||
from quizzi.infrastructure.database.models import User as UserModel
|
from quizzi.infrastructure.database.models import User as UserModel
|
||||||
from quizzi.infrastructure.database.models import UserAnswer as UserAnswerModel
|
from quizzi.infrastructure.database.models import UserAnswer as UserAnswerModel
|
||||||
from quizzi.infrastructure.utils.timezone import now_msk_naive
|
from quizzi.infrastructure.utils.timezone import now_utc_naive
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@@ -135,7 +135,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=now_msk_naive(),
|
finished_at=now_utc_naive(),
|
||||||
score=score,
|
score=score,
|
||||||
is_passed=is_passed
|
is_passed=is_passed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from quizzi.infrastructure.database.dao.test import TestDAO
|
|||||||
from quizzi.infrastructure.database.dao.user_answer import UserAnswerDAO
|
from quizzi.infrastructure.database.dao.user_answer import UserAnswerDAO
|
||||||
from quizzi.infrastructure.database.repo.test import TestRepository
|
from quizzi.infrastructure.database.repo.test import TestRepository
|
||||||
from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
||||||
from quizzi.infrastructure.utils.timezone import now_msk_naive
|
from quizzi.infrastructure.utils.timezone import now_utc_naive
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ async def deactivate_expired_tests(container: AsyncContainer) -> None:
|
|||||||
async with container() as request_container:
|
async with container() as request_container:
|
||||||
test_dao = await request_container.get(TestDAO)
|
test_dao = await request_container.get(TestDAO)
|
||||||
|
|
||||||
expired_tests = await test_dao.get_expired_active_tests(now_msk_naive())
|
expired_tests = await test_dao.get_expired_active_tests(now_utc_naive())
|
||||||
|
|
||||||
for test in expired_tests:
|
for test in expired_tests:
|
||||||
await test_dao.update(test.id, is_active=False)
|
await test_dao.update(test.id, is_active=False)
|
||||||
@@ -31,7 +31,7 @@ async def finish_expired_test_attempts(container: AsyncContainer, bot: Bot) -> N
|
|||||||
test_repo = await request_container.get(TestRepository)
|
test_repo = await request_container.get(TestRepository)
|
||||||
answer_dao = await request_container.get(UserAnswerDAO)
|
answer_dao = await request_container.get(UserAnswerDAO)
|
||||||
|
|
||||||
now = now_msk_naive()
|
now = now_utc_naive()
|
||||||
expired_attempts = await attempt_repo.get_expired_active_attempts(now)
|
expired_attempts = await attempt_repo.get_expired_active_attempts(now)
|
||||||
|
|
||||||
for attempt, _ in expired_attempts:
|
for attempt, _ in expired_attempts:
|
||||||
@@ -91,7 +91,7 @@ async def send_time_warning_notifications(container: AsyncContainer, bot: Bot) -
|
|||||||
attempt_repo = await request_container.get(TestAttemptRepository)
|
attempt_repo = await request_container.get(TestAttemptRepository)
|
||||||
test_repo = await request_container.get(TestRepository)
|
test_repo = await request_container.get(TestRepository)
|
||||||
|
|
||||||
now = now_msk_naive()
|
now = now_utc_naive()
|
||||||
attempts_needing_warning = await attempt_repo.get_attempts_needing_warning(now)
|
attempts_needing_warning = await attempt_repo.get_attempts_needing_warning(now)
|
||||||
|
|
||||||
for attempt, time_limit, questions_count in attempts_needing_warning:
|
for attempt, time_limit, questions_count in attempts_needing_warning:
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime, timedelta, timezone
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
MSK_TZ = ZoneInfo("Europe/Moscow")
|
MSK_TZ = timezone(timedelta(hours=3))
|
||||||
|
|
||||||
|
|
||||||
def now_msk() -> datetime:
|
def now_utc() -> datetime:
|
||||||
return datetime.now(MSK_TZ)
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def now_msk_naive() -> datetime:
|
|
||||||
return datetime.now(MSK_TZ).replace(tzinfo=None)
|
|
||||||
|
|
||||||
|
|
||||||
def now_utc_naive() -> datetime:
|
def now_utc_naive() -> datetime:
|
||||||
return datetime.utcnow()
|
return datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
def to_msk(dt: datetime | None) -> datetime | None:
|
def to_msk(dt: datetime | None) -> datetime | None:
|
||||||
if dt is None:
|
if dt is None:
|
||||||
return None
|
return None
|
||||||
if dt.tzinfo is None:
|
if dt.tzinfo is None:
|
||||||
return dt.replace(tzinfo=MSK_TZ)
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
return dt.astimezone(MSK_TZ)
|
return dt.astimezone(MSK_TZ)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from quizzi.infrastructure.database.repo.test import TestRepository
|
|||||||
from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
||||||
from quizzi.infrastructure.utils.config import Config
|
from quizzi.infrastructure.utils.config import Config
|
||||||
from quizzi.infrastructure.utils.test_id_to_hash import decode_id, encode_id
|
from quizzi.infrastructure.utils.test_id_to_hash import decode_id, encode_id
|
||||||
from quizzi.infrastructure.utils.timezone import now_msk_naive
|
from quizzi.infrastructure.utils.timezone import now_utc_naive
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -68,7 +68,7 @@ class TestService:
|
|||||||
if not test.is_active:
|
if not test.is_active:
|
||||||
return TestValidationResult(is_valid=False, error="❌ Тест деактивирован", test=test)
|
return TestValidationResult(is_valid=False, error="❌ Тест деактивирован", test=test)
|
||||||
|
|
||||||
if test.expires_at and test.expires_at < now_msk_naive():
|
if test.expires_at and test.expires_at < now_utc_naive():
|
||||||
return TestValidationResult(is_valid=False, error="❌ Срок действия теста истек", test=test)
|
return TestValidationResult(is_valid=False, error="❌ Срок действия теста истек", test=test)
|
||||||
|
|
||||||
user = await self._user_dao.get_by_id(user_id)
|
user = await self._user_dao.get_by_id(user_id)
|
||||||
@@ -90,7 +90,7 @@ class TestService:
|
|||||||
if not test.is_active:
|
if not test.is_active:
|
||||||
return TestAccessResult(can_access=False, error="❌ Тест деактивирован")
|
return TestAccessResult(can_access=False, error="❌ Тест деактивирован")
|
||||||
|
|
||||||
if test.expires_at and test.expires_at < now_msk_naive():
|
if test.expires_at and test.expires_at < now_utc_naive():
|
||||||
return TestAccessResult(can_access=False, error="❌ Срок действия теста истек")
|
return TestAccessResult(can_access=False, error="❌ Срок действия теста истек")
|
||||||
|
|
||||||
if test.attempts:
|
if test.attempts:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from quizzi.infrastructure.database.dao.test import TestDAO
|
|||||||
from quizzi.infrastructure.database.dao.user_answer import UserAnswerDAO
|
from quizzi.infrastructure.database.dao.user_answer import UserAnswerDAO
|
||||||
from quizzi.infrastructure.database.repo.test import TestRepository
|
from quizzi.infrastructure.database.repo.test import TestRepository
|
||||||
from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository
|
||||||
from quizzi.infrastructure.utils.timezone import now_msk_naive
|
from quizzi.infrastructure.utils.timezone import now_utc_naive
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -56,7 +56,7 @@ class TestAttemptService:
|
|||||||
return AttemptStartResult(success=False, error="❌ В тесте нет вопросов")
|
return AttemptStartResult(success=False, error="❌ В тесте нет вопросов")
|
||||||
|
|
||||||
attempt = await self._attempt_repo.attempt_dao.create(user_id=user_id, test_id=test_id)
|
attempt = await self._attempt_repo.attempt_dao.create(user_id=user_id, test_id=test_id)
|
||||||
started_at = now_msk_naive()
|
started_at = now_utc_naive()
|
||||||
|
|
||||||
return AttemptStartResult(
|
return AttemptStartResult(
|
||||||
success=True,
|
success=True,
|
||||||
|
|||||||
Reference in New Issue
Block a user