diff --git a/src/quizzi/application/bot/shared_dialogs/broadcast.py b/src/quizzi/application/bot/shared_dialogs/broadcast.py index 132204c..6c1c633 100644 --- a/src/quizzi/application/bot/shared_dialogs/broadcast.py +++ b/src/quizzi/application/bot/shared_dialogs/broadcast.py @@ -1,5 +1,3 @@ -from typing import TYPE_CHECKING - from aiogram.types import CallbackQuery, Message from aiogram_dialog import Dialog, DialogManager, Window from aiogram_dialog.widgets.input import MessageInput diff --git a/src/quizzi/application/bot/user_dialogs/take_test.py b/src/quizzi/application/bot/user_dialogs/take_test.py index 807d14e..a34992a 100644 --- a/src/quizzi/application/bot/user_dialogs/take_test.py +++ b/src/quizzi/application/bot/user_dialogs/take_test.py @@ -18,7 +18,7 @@ from quizzi.infrastructure.database.dao.user_answer import UserAnswerDAO from quizzi.infrastructure.database.repo.test import TestRepository from quizzi.infrastructure.database.repo.test_attempt import TestAttemptRepository from quizzi.infrastructure.utils.rate_limiter import PasswordRateLimiter -from quizzi.infrastructure.utils.timezone import now_msk_naive +from quizzi.infrastructure.utils.timezone import now_utc_naive async def get_state_for_question_type(question_type: str): @@ -33,7 +33,7 @@ async def get_state_for_question_type(question_type: str): def get_remaining_time(started_at: datetime, time_limit: int) -> int | None: if not time_limit: return None - elapsed = (now_msk_naive() - started_at).total_seconds() + elapsed = (now_utc_naive() - started_at).total_seconds() remaining = time_limit - elapsed return max(0, int(remaining)) @@ -308,7 +308,7 @@ async def start_test_directly( return 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_state = await get_state_for_question_type(first_question.question_type if first_question else QuestionType.SINGLE) @@ -364,7 +364,7 @@ async def on_password_input( return attempt = await attempt_repo.attempt_dao.create(user_id=message.from_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_state = await get_state_for_question_type(first_question.question_type if first_question else QuestionType.SINGLE) diff --git a/src/quizzi/infrastructure/database/dao/test.py b/src/quizzi/infrastructure/database/dao/test.py index 44a4069..52f2d1c 100644 --- a/src/quizzi/infrastructure/database/dao/test.py +++ b/src/quizzi/infrastructure/database/dao/test.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import NotRequired, TypedDict, Unpack from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -9,25 +8,6 @@ from quizzi.infrastructure.database.dto.test import TestDTO from quizzi.infrastructure.database.models import Test -class _UNSET: - pass - - -UNSET = _UNSET() - - -class TestUpdateFields(TypedDict, total=False): - title: str - description: str | None - for_group: int | None - password: str | None - expires_at: datetime | None - attempts: int | None - time_limit: int | None - is_active: bool - are_results_viewable: bool - - class TestDAO: def __init__(self, session: AsyncSession) -> None: self.session: AsyncSession = session @@ -87,15 +67,15 @@ class TestDAO: async def update( self, test_id: int, - title: str | _UNSET = UNSET, - description: str | None | _UNSET = UNSET, - for_group: int | None | _UNSET = UNSET, - 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, + title: str | None = None, + description: str | None = None, + for_group: int | None = None, + password: str | None = None, + expires_at: datetime | None = None, + attempts: int | None = None, + time_limit: int | None = None, + is_active: bool | None = None, + are_results_viewable: bool | None = None, ) -> DomainTest | None: result = await self.session.execute( select(Test).where(Test.id == test_id) @@ -104,23 +84,23 @@ class TestDAO: if not test: return None - if not isinstance(title, _UNSET): + if title is not None: test.title = title - if not isinstance(description, _UNSET): + if description is not None: test.description = description - if not isinstance(for_group, _UNSET): + if for_group is not None: test.for_group = for_group - if not isinstance(password, _UNSET): + if password is not None: test.password = password - if not isinstance(expires_at, _UNSET): + if expires_at is not None: test.expires_at = expires_at - if not isinstance(attempts, _UNSET): + if attempts is not None: test.attempts = attempts - if not isinstance(time_limit, _UNSET): + if time_limit is not None: test.time_limit = time_limit - if not isinstance(is_active, _UNSET): + if is_active is not None: test.is_active = is_active - if not isinstance(are_results_viewable, _UNSET): + if are_results_viewable is not None: test.are_results_viewable = are_results_viewable await self.session.flush() diff --git a/src/quizzi/infrastructure/database/dao/test_attempt.py b/src/quizzi/infrastructure/database/dao/test_attempt.py index a11de46..0916bbb 100644 --- a/src/quizzi/infrastructure/database/dao/test_attempt.py +++ b/src/quizzi/infrastructure/database/dao/test_attempt.py @@ -34,6 +34,7 @@ class TestAttemptDAO: attempt = TestAttempt( user_id=user_id, test_id=test_id, + started_at=datetime.utcnow(), score=score, is_passed=is_passed, ) diff --git a/src/quizzi/infrastructure/database/dao/user.py b/src/quizzi/infrastructure/database/dao/user.py index d7cc091..08b6d30 100644 --- a/src/quizzi/infrastructure/database/dao/user.py +++ b/src/quizzi/infrastructure/database/dao/user.py @@ -8,13 +8,6 @@ from quizzi.infrastructure.database.dto.user import UserDTO from quizzi.infrastructure.database.models import User -class _UNSET: - pass - - -UNSET = _UNSET() - - class UserDAO: def __init__(self, session: AsyncSession) -> None: self.session: AsyncSession = session @@ -74,14 +67,14 @@ class UserDAO: async def update( self, user_id: int, - username: str | None | _UNSET = UNSET, - first_name: str | _UNSET = UNSET, - last_name: str | None | _UNSET = UNSET, - name: str | None | _UNSET = UNSET, - group: int | None | _UNSET = UNSET, - is_admin: bool | _UNSET = UNSET, - name_updated_at: datetime | None | _UNSET = UNSET, - group_updated_at: datetime | None | _UNSET = UNSET, + username: str | None = None, + first_name: str | None = None, + last_name: str | None = None, + name: str | None = None, + group: int | None = None, + is_admin: bool | None = None, + name_updated_at: datetime | None = None, + group_updated_at: datetime | None = None, ) -> DomainUser | None: result = await self.session.execute( select(User).where(User.id == user_id) @@ -90,21 +83,21 @@ class UserDAO: if not user: return None - if not isinstance(username, _UNSET): + if username is not None: user.username = username - if not isinstance(first_name, _UNSET): + if first_name is not None: user.first_name = first_name - if not isinstance(last_name, _UNSET): + if last_name is not None: user.last_name = last_name - if not isinstance(name, _UNSET): + if name is not None: user.name = name - if not isinstance(group, _UNSET): + if group is not None: user.group = group - if not isinstance(is_admin, _UNSET): + if is_admin is not None: user.is_admin = is_admin - if not isinstance(name_updated_at, _UNSET): + if name_updated_at is not None: user.name_updated_at = name_updated_at - if not isinstance(group_updated_at, _UNSET): + if group_updated_at is not None: user.group_updated_at = group_updated_at await self.session.flush() @@ -129,9 +122,9 @@ class UserDAO: first_name: str, username: str | None = None, last_name: str | None = None, - name: str | None | _UNSET = UNSET, - group: int | None | _UNSET = UNSET, - is_admin: bool | _UNSET = UNSET, + name: str | None = None, + group: int | None = None, + is_admin: bool | None = None, ) -> DomainUser: result = await self.session.execute( select(User).where(User.id == user_id) @@ -142,11 +135,11 @@ class UserDAO: user.username = username user.first_name = first_name user.last_name = last_name - if not isinstance(name, _UNSET): + if name is not None: user.name = name - if not isinstance(group, _UNSET): + if group is not None: user.group = group - if not isinstance(is_admin, _UNSET): + if is_admin is not None: user.is_admin = is_admin await self.session.flush() await self.session.refresh(user) @@ -157,7 +150,7 @@ class UserDAO: username=username, first_name=first_name, last_name=last_name, - name=name if not isinstance(name, _UNSET) else None, - group=group if not isinstance(group, _UNSET) else None, - is_admin=is_admin if not isinstance(is_admin, _UNSET) else False, + name=name, + group=group, + is_admin=is_admin if is_admin is not None else False, ) diff --git a/src/quizzi/infrastructure/utils/timezone.py b/src/quizzi/infrastructure/utils/timezone.py index 7201d7c..8d483e5 100644 --- a/src/quizzi/infrastructure/utils/timezone.py +++ b/src/quizzi/infrastructure/utils/timezone.py @@ -12,6 +12,10 @@ def now_msk_naive() -> datetime: return datetime.now(MSK_TZ).replace(tzinfo=None) +def now_utc_naive() -> datetime: + return datetime.utcnow() + + def to_msk(dt: datetime | None) -> datetime | None: if dt is None: return None