diff --git a/alembic/env.py b/alembic/env.py index f674492..b1eccfe 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -7,9 +7,9 @@ from alembic import context from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import async_engine_from_config -from src.dutylog.infrastructure.database.models.base import Base -from src.dutylog.infrastructure.database.models.user import User as User -from src.dutylog.infrastructure.utils.config import load_config +from dutylog.infrastructure.database.models.base import Base +from dutylog.infrastructure.database.models import * +from dutylog.infrastructure.utils.config import load_config config = context.config diff --git a/alembic/versions/fc1269fbc645_add_models.py b/alembic/versions/fc1269fbc645_add_models.py new file mode 100644 index 0000000..44148b8 --- /dev/null +++ b/alembic/versions/fc1269fbc645_add_models.py @@ -0,0 +1,56 @@ +"""add models + +Revision ID: fc1269fbc645 +Revises: f012f3ef4b65 +Create Date: 2026-02-27 17:24:00.349948 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'fc1269fbc645' +down_revision: Union[str, Sequence[str], None] = 'f012f3ef4b65' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('hours_transactions', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.BigInteger(), nullable=False), + sa.Column('transaction_type', sa.String(length=50), nullable=False), + sa.Column('amount', sa.Integer(), nullable=False), + sa.Column('admin_id', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['admin_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('users', sa.Column('first_name', sa.String(length=255), nullable=True)) + op.add_column('users', sa.Column('last_name', sa.String(length=255), nullable=True)) + op.add_column('users', sa.Column('is_admin', sa.Boolean(), server_default='false', nullable=False)) + op.add_column('users', sa.Column('active_hours', sa.Integer(), server_default='0', nullable=False)) + op.add_column('users', sa.Column('inactive_hours', sa.Integer(), server_default='0', nullable=False)) + op.add_column('users', sa.Column('created_at', sa.DateTime(timezone=True), nullable=False)) + op.add_column('users', sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'updated_at') + op.drop_column('users', 'created_at') + op.drop_column('users', 'inactive_hours') + op.drop_column('users', 'active_hours') + op.drop_column('users', 'is_admin') + op.drop_column('users', 'last_name') + op.drop_column('users', 'first_name') + op.drop_table('hours_transactions') + # ### end Alembic commands ### diff --git a/src/dutylog/application/__main__.py b/src/dutylog/application/__main__.py index 1f2f585..7039801 100644 --- a/src/dutylog/application/__main__.py +++ b/src/dutylog/application/__main__.py @@ -8,10 +8,10 @@ from aiogram_dialog import setup_dialogs from dishka import make_async_container from dishka.integrations.aiogram import setup_dishka -from src.dutylog.application.bot.user_handlers import router as user_router -from src.dutylog.application.bot.user_dialogs import main_menu_dialog -from src.dutylog.infrastructure.ioc import ConfigProvider, DatabaseProvider, DAOProvider -from src.dutylog.infrastructure.utils.config import load_config +from dutylog.application.bot.user_handlers import router as user_router +from dutylog.application.bot.user_dialogs import main_menu_dialog +from dutylog.infrastructure.ioc import ConfigProvider, DatabaseProvider, DAOProvider +from dutylog.infrastructure.utils.config import load_config async def main(): diff --git a/src/dutylog/application/bot/user_dialogs/__init__.py b/src/dutylog/application/bot/user_dialogs/__init__.py index 1c26add..6c8bab4 100644 --- a/src/dutylog/application/bot/user_dialogs/__init__.py +++ b/src/dutylog/application/bot/user_dialogs/__init__.py @@ -1,3 +1,3 @@ -from src.dutylog.application.bot.user_dialogs.main_menu_dialog import main_menu_dialog +from dutylog.application.bot.user_dialogs.main_menu_dialog import main_menu_dialog __all__ = ["main_menu_dialog"] diff --git a/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py b/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py index e3b14fa..bbaa286 100644 --- a/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py @@ -1,7 +1,7 @@ from aiogram_dialog import Dialog, Window from aiogram_dialog.widgets.text import Const -from src.dutylog.application.bot.user_dialogs.states import MainMenuSG +from dutylog.application.bot.user_dialogs.states import MainMenuSG main_menu_dialog = Dialog( Window( diff --git a/src/dutylog/application/bot/user_handlers.py b/src/dutylog/application/bot/user_handlers.py index a77522b..bfafa9e 100644 --- a/src/dutylog/application/bot/user_handlers.py +++ b/src/dutylog/application/bot/user_handlers.py @@ -3,7 +3,7 @@ from aiogram.filters import CommandStart from aiogram.types import Message from aiogram_dialog import DialogManager, StartMode -from src.dutylog.application.bot.user_dialogs.states import MainMenuSG +from dutylog.application.bot.user_dialogs.states import MainMenuSG router = Router() diff --git a/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py b/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py new file mode 100644 index 0000000..174886f --- /dev/null +++ b/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py @@ -0,0 +1,41 @@ +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession + +from dutylog.infrastructure.database.models.hours_transaction import HoursTransaction + + +class HoursTransactionsDAO: + def __init__(self, session: AsyncSession): + self.session = session + + async def get_by_id(self, transaction_id: int) -> HoursTransaction | None: + result = await self.session.execute( + select(HoursTransaction).where(HoursTransaction.id == transaction_id) + ) + return result.scalar_one_or_none() + + async def get_all(self) -> list[HoursTransaction]: + result = await self.session.execute( + select(HoursTransaction).order_by(HoursTransaction.created_at.desc()) + ) + return list(result.scalars().all()) + + async def get_by_user_id(self, user_id: int) -> list[HoursTransaction]: + result = await self.session.execute( + select(HoursTransaction) + .where(HoursTransaction.user_id == user_id) + .order_by(HoursTransaction.created_at.desc()) + ) + return list(result.scalars().all()) + + async def create(self, transaction: HoursTransaction) -> HoursTransaction: + self.session.add(transaction) + await self.session.commit() + await self.session.refresh(transaction) + return transaction + + async def delete(self, transaction_id: int) -> None: + await self.session.execute( + delete(HoursTransaction).where(HoursTransaction.id == transaction_id) + ) + await self.session.commit() diff --git a/src/dutylog/infrastructure/database/dao/users_dao.py b/src/dutylog/infrastructure/database/dao/users_dao.py index 1e8a30d..af43e9f 100644 --- a/src/dutylog/infrastructure/database/dao/users_dao.py +++ b/src/dutylog/infrastructure/database/dao/users_dao.py @@ -1,21 +1,38 @@ -from sqlalchemy import select +from sqlalchemy import select, update, delete from sqlalchemy.ext.asyncio import AsyncSession -from src.dutylog.infrastructure.database.models.user import User +from dutylog.infrastructure.database.models.user import User class UsersDAO: def __init__(self, session: AsyncSession): self.session = session - async def get_user(self, user_id: int) -> User | None: + async def get_by_id(self, user_id: int) -> User | None: result = await self.session.execute( select(User).where(User.id == user_id) ) return result.scalar_one_or_none() - async def create_user(self, user_id: int, username: str | None) -> User: - user = User(id=user_id, username=username) + async def get_all(self) -> list[User]: + result = await self.session.execute(select(User)) + return list(result.scalars().all()) + + async def create(self, user: User) -> User: self.session.add(user) await self.session.commit() + await self.session.refresh(user) return user + + async def update(self, user_id: int, **kwargs) -> User | None: + await self.session.execute( + update(User).where(User.id == user_id).values(**kwargs) + ) + await self.session.commit() + return await self.get_by_id(user_id) + + async def delete(self, user_id: int) -> None: + await self.session.execute( + delete(User).where(User.id == user_id) + ) + await self.session.commit() diff --git a/src/dutylog/infrastructure/database/models/__init__.py b/src/dutylog/infrastructure/database/models/__init__.py index e69de29..5c24032 100644 --- a/src/dutylog/infrastructure/database/models/__init__.py +++ b/src/dutylog/infrastructure/database/models/__init__.py @@ -0,0 +1,5 @@ +from dutylog.infrastructure.database.models.base import Base +from dutylog.infrastructure.database.models.user import User +from dutylog.infrastructure.database.models.hours_transaction import HoursTransaction + +__all__ = ["Base", "User", "HoursTransaction"] diff --git a/src/dutylog/infrastructure/database/models/hours_transaction.py b/src/dutylog/infrastructure/database/models/hours_transaction.py new file mode 100644 index 0000000..8a99924 --- /dev/null +++ b/src/dutylog/infrastructure/database/models/hours_transaction.py @@ -0,0 +1,24 @@ +from datetime import datetime, timezone, timedelta +from enum import Enum + +from sqlalchemy import BigInteger, Integer, String, DateTime, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column + +from dutylog.infrastructure.database.models.base import Base +from dutylog.infrastructure.utils.datetime import msk_now + + +class TransactionType(str, Enum): + INCREASE = "increase" + DECREASE = "decrease" + + +class HoursTransaction(Base): + __tablename__ = "hours_transactions" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + transaction_type: Mapped[str] = mapped_column(String(50), nullable=False) + amount: Mapped[int] = mapped_column(Integer, nullable=False) + admin_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now) diff --git a/src/dutylog/infrastructure/database/models/user.py b/src/dutylog/infrastructure/database/models/user.py index 26dd6e5..3250cc4 100644 --- a/src/dutylog/infrastructure/database/models/user.py +++ b/src/dutylog/infrastructure/database/models/user.py @@ -1,7 +1,10 @@ -from sqlalchemy import BigInteger, String +from datetime import datetime + +from sqlalchemy import BigInteger, Boolean, Integer, String, DateTime from sqlalchemy.orm import Mapped, mapped_column -from src.dutylog.infrastructure.database.models.base import Base +from dutylog.infrastructure.database.models.base import Base +from dutylog.infrastructure.utils.datetime import msk_now class User(Base): @@ -9,3 +12,10 @@ class User(Base): id: Mapped[int] = mapped_column(BigInteger, primary_key=True) username: Mapped[str | None] = mapped_column(String(255), nullable=True) + first_name: Mapped[str | None] = mapped_column(String(255), nullable=True) + last_name: Mapped[str | None] = mapped_column(String(255), nullable=True) + is_admin: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") + active_hours: Mapped[int] = mapped_column(Integer, default=0, server_default="0") + inactive_hours: Mapped[int] = mapped_column(Integer, default=0, server_default="0") + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=msk_now, onupdate=msk_now) diff --git a/src/dutylog/infrastructure/ioc.py b/src/dutylog/infrastructure/ioc.py index 6505abe..3bde9f7 100644 --- a/src/dutylog/infrastructure/ioc.py +++ b/src/dutylog/infrastructure/ioc.py @@ -5,6 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker from dutylog.infrastructure.database.config import create_engine, create_session_maker from dutylog.infrastructure.database.dao.users_dao import UsersDAO +from dutylog.infrastructure.database.dao.hours_transactions_dao import HoursTransactionsDAO from dutylog.infrastructure.utils.config import Config, load_config @@ -36,3 +37,8 @@ class DAOProvider(Provider): def get_users_dao(self, session: AsyncSession) -> UsersDAO: return UsersDAO(session) + @provide(scope=Scope.REQUEST) + def get_hours_transactions_dao(self, session: AsyncSession) -> HoursTransactionsDAO: + return HoursTransactionsDAO(session) + + diff --git a/src/dutylog/infrastructure/utils/datetime.py b/src/dutylog/infrastructure/utils/datetime.py new file mode 100644 index 0000000..0a9901a --- /dev/null +++ b/src/dutylog/infrastructure/utils/datetime.py @@ -0,0 +1,8 @@ +from datetime import datetime, timezone, timedelta + + +MSK_TZ = timezone(timedelta(hours=3)) + + +def msk_now() -> datetime: + return datetime.now(MSK_TZ)