This commit is contained in:
2026-02-20 00:53:03 +03:00
parent f6d0e18605
commit 0e111c31cb
6 changed files with 52 additions and 76 deletions
@@ -1,5 +1,3 @@
from typing import TYPE_CHECKING
from aiogram.types import CallbackQuery, Message from aiogram.types import CallbackQuery, Message
from aiogram_dialog import Dialog, DialogManager, Window from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.input import MessageInput from aiogram_dialog.widgets.input import MessageInput
@@ -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 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 from quizzi.infrastructure.utils.timezone import now_utc_naive
async def get_state_for_question_type(question_type: str): 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: def get_remaining_time(started_at: datetime, time_limit: int) -> int | None:
if not time_limit: if not time_limit:
return None return None
elapsed = (now_msk_naive() - started_at).total_seconds() elapsed = (now_utc_naive() - started_at).total_seconds()
remaining = time_limit - elapsed remaining = time_limit - elapsed
return max(0, int(remaining)) return max(0, int(remaining))
@@ -308,7 +308,7 @@ async def start_test_directly(
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)
first_state = await get_state_for_question_type(first_question.question_type if first_question else QuestionType.SINGLE) 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 return
attempt = await attempt_repo.attempt_dao.create(user_id=message.from_user.id, test_id=test_id) 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_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) first_state = await get_state_for_question_type(first_question.question_type if first_question else QuestionType.SINGLE)
+18 -38
View File
@@ -1,5 +1,4 @@
from datetime import datetime from datetime import datetime
from typing import NotRequired, TypedDict, Unpack
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession 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 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: class TestDAO:
def __init__(self, session: AsyncSession) -> None: def __init__(self, session: AsyncSession) -> None:
self.session: AsyncSession = session self.session: AsyncSession = session
@@ -87,15 +67,15 @@ class TestDAO:
async def update( async def update(
self, self,
test_id: int, test_id: int,
title: str | _UNSET = UNSET, title: str | None = None,
description: str | None | _UNSET = UNSET, description: str | None = None,
for_group: int | None | _UNSET = UNSET, for_group: int | None = None,
password: str | None | _UNSET = UNSET, password: str | None = None,
expires_at: datetime | None | _UNSET = UNSET, expires_at: datetime | None = None,
attempts: int | None | _UNSET = UNSET, attempts: int | None = None,
time_limit: int | None | _UNSET = UNSET, time_limit: int | None = None,
is_active: bool | _UNSET = UNSET, is_active: bool | None = None,
are_results_viewable: bool | _UNSET = UNSET, are_results_viewable: bool | None = None,
) -> DomainTest | None: ) -> DomainTest | None:
result = await self.session.execute( result = await self.session.execute(
select(Test).where(Test.id == test_id) select(Test).where(Test.id == test_id)
@@ -104,23 +84,23 @@ class TestDAO:
if not test: if not test:
return None return None
if not isinstance(title, _UNSET): if title is not None:
test.title = title test.title = title
if not isinstance(description, _UNSET): if description is not None:
test.description = description test.description = description
if not isinstance(for_group, _UNSET): if for_group is not None:
test.for_group = for_group test.for_group = for_group
if not isinstance(password, _UNSET): if password is not None:
test.password = password test.password = password
if not isinstance(expires_at, _UNSET): if expires_at is not None:
test.expires_at = expires_at test.expires_at = expires_at
if not isinstance(attempts, _UNSET): if attempts is not None:
test.attempts = attempts test.attempts = attempts
if not isinstance(time_limit, _UNSET): if time_limit is not None:
test.time_limit = time_limit test.time_limit = time_limit
if not isinstance(is_active, _UNSET): if is_active is not None:
test.is_active = is_active 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 test.are_results_viewable = are_results_viewable
await self.session.flush() await self.session.flush()
@@ -34,6 +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(),
score=score, score=score,
is_passed=is_passed, is_passed=is_passed,
) )
+25 -32
View File
@@ -8,13 +8,6 @@ from quizzi.infrastructure.database.dto.user import UserDTO
from quizzi.infrastructure.database.models import User from quizzi.infrastructure.database.models import User
class _UNSET:
pass
UNSET = _UNSET()
class UserDAO: class UserDAO:
def __init__(self, session: AsyncSession) -> None: def __init__(self, session: AsyncSession) -> None:
self.session: AsyncSession = session self.session: AsyncSession = session
@@ -74,14 +67,14 @@ class UserDAO:
async def update( async def update(
self, self,
user_id: int, user_id: int,
username: str | None | _UNSET = UNSET, username: str | None = None,
first_name: str | _UNSET = UNSET, first_name: str | None = None,
last_name: str | None | _UNSET = UNSET, last_name: str | None = None,
name: str | None | _UNSET = UNSET, name: str | None = None,
group: int | None | _UNSET = UNSET, group: int | None = None,
is_admin: bool | _UNSET = UNSET, is_admin: bool | None = None,
name_updated_at: datetime | None | _UNSET = UNSET, name_updated_at: datetime | None = None,
group_updated_at: datetime | None | _UNSET = UNSET, group_updated_at: datetime | None = None,
) -> DomainUser | None: ) -> DomainUser | None:
result = await self.session.execute( result = await self.session.execute(
select(User).where(User.id == user_id) select(User).where(User.id == user_id)
@@ -90,21 +83,21 @@ class UserDAO:
if not user: if not user:
return None return None
if not isinstance(username, _UNSET): if username is not None:
user.username = username user.username = username
if not isinstance(first_name, _UNSET): if first_name is not None:
user.first_name = first_name user.first_name = first_name
if not isinstance(last_name, _UNSET): if last_name is not None:
user.last_name = last_name user.last_name = last_name
if not isinstance(name, _UNSET): if name is not None:
user.name = name user.name = name
if not isinstance(group, _UNSET): if group is not None:
user.group = group user.group = group
if not isinstance(is_admin, _UNSET): if is_admin is not None:
user.is_admin = is_admin 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 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 user.group_updated_at = group_updated_at
await self.session.flush() await self.session.flush()
@@ -129,9 +122,9 @@ class UserDAO:
first_name: str, first_name: str,
username: str | None = None, username: str | None = None,
last_name: str | None = None, last_name: str | None = None,
name: str | None | _UNSET = UNSET, name: str | None = None,
group: int | None | _UNSET = UNSET, group: int | None = None,
is_admin: bool | _UNSET = UNSET, 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)
@@ -142,11 +135,11 @@ class UserDAO:
user.username = username user.username = username
user.first_name = first_name user.first_name = first_name
user.last_name = last_name user.last_name = last_name
if not isinstance(name, _UNSET): if name is not None:
user.name = name user.name = name
if not isinstance(group, _UNSET): if group is not None:
user.group = group user.group = group
if not isinstance(is_admin, _UNSET): if is_admin is not None:
user.is_admin = is_admin user.is_admin = is_admin
await self.session.flush() await self.session.flush()
await self.session.refresh(user) await self.session.refresh(user)
@@ -157,7 +150,7 @@ class UserDAO:
username=username, username=username,
first_name=first_name, first_name=first_name,
last_name=last_name, last_name=last_name,
name=name if not isinstance(name, _UNSET) else None, name=name,
group=group if not isinstance(group, _UNSET) else None, group=group,
is_admin=is_admin if not isinstance(is_admin, _UNSET) else False, is_admin=is_admin if is_admin is not None else False,
) )
@@ -12,6 +12,10 @@ def now_msk_naive() -> datetime:
return datetime.now(MSK_TZ).replace(tzinfo=None) return datetime.now(MSK_TZ).replace(tzinfo=None)
def now_utc_naive() -> datetime:
return datetime.utcnow()
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