From d117776e650f160c980fb722379ce9c57a031cc1 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Mon, 1 Dec 2025 13:34:26 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=20=D0=B1=D1=83=D0=BB=D0=B5=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BD=D0=B0=20status=20=D0=B4=D0=BB=D1=8F=20target=5F?= =?UTF-8?q?channel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/adapter/postgres.py | 4 ++-- .../telegram_callback/chat_join_request.py | 5 ----- src/domain/subscriber.py | 3 --- src/domain/target_channel.py | 10 ---------- src/dto/target_channel.py | 4 ++++ .../target_channel/connect_target_chan.py | 5 +++-- .../target_channel/disconnect_target_chan.py | 2 +- .../disconnect_target_chan_by_tg_id.py | 2 +- .../target_channel/get_user_target_chans.py | 5 +++-- .../update_target_chan_permissions.py | 8 ++++---- tests/test_creatives.py | 14 ++++++------- tests/test_external_channels.py | 10 +++++----- tests/test_placements.py | 20 +++++++++---------- tests/test_subscriptions.py | 8 ++++---- tests/test_target_channels.py | 11 +++++----- tests/test_views.py | 12 +++++------ 16 files changed, 56 insertions(+), 67 deletions(-) diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index 8f1b32f..929bee7 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -10,7 +10,7 @@ from src import domain class Postgres(DatabaseBase): - async def get_user(self, *, user_id: uuid.UUID | None = None, telegram_id: int | None = None) -> domain.User | None: + async def get_user(self, user_id: uuid.UUID | None = None, telegram_id: int | None = None) -> domain.User | None: if user_id: return await self.session.get(domain.User, user_id) @@ -85,7 +85,7 @@ class Postgres(DatabaseBase): existing.title = channel.title existing.username = channel.username existing.user_id = channel.user_id - existing.is_active = channel.is_active + existing.status = channel.status existing.deleted_at = None existing.updated_at = datetime.datetime.now(datetime.UTC) diff --git a/src/controller/telegram_callback/chat_join_request.py b/src/controller/telegram_callback/chat_join_request.py index 448f1f5..4dd2ff0 100644 --- a/src/controller/telegram_callback/chat_join_request.py +++ b/src/controller/telegram_callback/chat_join_request.py @@ -10,11 +10,6 @@ log = logging.getLogger(__name__) @telegram_callback_router.chat_join_request() async def on_chat_join_request(event: ChatJoinRequest) -> None: - """ - Обрабатывает запросы на вступление в канал через приватную invite_link с одобрением. - - Событие приходит когда пользователь отправляет запрос на вступление в канал. - """ if event.chat.type not in {'channel', 'supergroup'}: return diff --git a/src/domain/subscriber.py b/src/domain/subscriber.py index 5da6ba7..ead83e3 100644 --- a/src/domain/subscriber.py +++ b/src/domain/subscriber.py @@ -10,12 +10,9 @@ if TYPE_CHECKING: class Subscriber(domain.Base): - """Пользователь Telegram, подписавшийся через invite links.""" - telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True) username: Mapped[str | None] first_name: Mapped[str | None] last_name: Mapped[str | None] - # Relationships subscriptions: Mapped[list['Subscription']] = relationship(back_populates='subscriber') diff --git a/src/domain/target_channel.py b/src/domain/target_channel.py index 96a0696..75b4651 100644 --- a/src/domain/target_channel.py +++ b/src/domain/target_channel.py @@ -25,22 +25,12 @@ class TargetChannel(domain.Base): username: Mapped[str | None] = mapped_column(index=True) status: Mapped[TargetChannelStatus] = mapped_column(default=TargetChannelStatus.ACTIVE) - # Relationship with user user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('user.id'), index=True) user: Mapped['User'] = relationship(back_populates='target_channels') - # M2M relationship with external channels external_channels: Mapped[list['ExternalChannel']] = relationship( secondary='target_external_channel', back_populates='target_channels', ) creatives: Mapped[list['Creative']] = relationship(back_populates='target_channel') - - @property - def is_active(self) -> bool: - return self.status == TargetChannelStatus.ACTIVE - - @is_active.setter - def is_active(self, value: bool) -> None: - self.status = TargetChannelStatus.ACTIVE if value else TargetChannelStatus.INACTIVE diff --git a/src/dto/target_channel.py b/src/dto/target_channel.py index 0e4d9b8..5979672 100644 --- a/src/dto/target_channel.py +++ b/src/dto/target_channel.py @@ -2,6 +2,8 @@ import uuid import pydantic +from src.domain.target_channel import TargetChannelStatus + class ChannelBotPermissions(pydantic.BaseModel): is_admin: bool @@ -28,6 +30,8 @@ class ConnectTargetChanOutput(pydantic.BaseModel): telegram_id: int title: str username: str | None + status: TargetChannelStatus + # Deprecated: используйте status. Оставлено для обратной совместимости с фронтендом is_active: bool diff --git a/src/usecase/target_channel/connect_target_chan.py b/src/usecase/target_channel/connect_target_chan.py index 9b2e9ee..63c9318 100644 --- a/src/usecase/target_channel/connect_target_chan.py +++ b/src/usecase/target_channel/connect_target_chan.py @@ -57,7 +57,7 @@ async def connect_target_chan(self: 'Usecase', input: dto.ConnectTargetChanInput title=input.title, username=input.username, user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) upserted_channel = await self.database.upsert_target_channel(channel) @@ -73,5 +73,6 @@ async def connect_target_chan(self: 'Usecase', input: dto.ConnectTargetChanInput telegram_id=upserted_channel.telegram_id, title=upserted_channel.title, username=upserted_channel.username, - is_active=upserted_channel.is_active, + status=upserted_channel.status, + is_active=upserted_channel.status == domain.TargetChannelStatus.ACTIVE, ) diff --git a/src/usecase/target_channel/disconnect_target_chan.py b/src/usecase/target_channel/disconnect_target_chan.py index 08138b8..1ce41f5 100644 --- a/src/usecase/target_channel/disconnect_target_chan.py +++ b/src/usecase/target_channel/disconnect_target_chan.py @@ -15,7 +15,7 @@ async def disconnect_target_chan(self: 'Usecase', input: dto.DisconnectTargetCha if not channel: raise domain.TargetChannelNotFound(input.channel_id) - channel.is_active = False + channel.status = domain.TargetChannelStatus.INACTIVE await self.database.update_target_channel(channel) log.info(f'Target channel {input.channel_id} deactivated') diff --git a/src/usecase/target_channel/disconnect_target_chan_by_tg_id.py b/src/usecase/target_channel/disconnect_target_chan_by_tg_id.py index 6b2a28e..85b781c 100644 --- a/src/usecase/target_channel/disconnect_target_chan_by_tg_id.py +++ b/src/usecase/target_channel/disconnect_target_chan_by_tg_id.py @@ -21,7 +21,7 @@ async def disconnect_target_chan_by_tg_id(self: 'Usecase', input: dto.Disconnect log.warning(f'Target channel {input.telegram_id} not found') raise domain.TargetChannelNotFound() - channel.is_active = False + channel.status = domain.TargetChannelStatus.INACTIVE await self.database.update_target_channel(channel) log.info(f'Target channel {input.telegram_id} deactivated') diff --git a/src/usecase/target_channel/get_user_target_chans.py b/src/usecase/target_channel/get_user_target_chans.py index eff9ffa..0ffa092 100644 --- a/src/usecase/target_channel/get_user_target_chans.py +++ b/src/usecase/target_channel/get_user_target_chans.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from src import dto +from src import domain, dto if TYPE_CHECKING: from .. import Usecase @@ -17,7 +17,8 @@ async def get_user_target_chans(self: 'Usecase', input: dto.GetUserTargetChansIn telegram_id=channel.telegram_id, title=channel.title, username=channel.username, - is_active=channel.is_active, + status=channel.status, + is_active=channel.status == domain.TargetChannelStatus.ACTIVE, ) for channel in channels ] diff --git a/src/usecase/target_channel/update_target_chan_permissions.py b/src/usecase/target_channel/update_target_chan_permissions.py index dec7918..acc5135 100644 --- a/src/usecase/target_channel/update_target_chan_permissions.py +++ b/src/usecase/target_channel/update_target_chan_permissions.py @@ -29,8 +29,8 @@ async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTarge raise domain.TargetChannelNotFound() if not missing_permissions: - if not channel.is_active: - channel.is_active = True + if channel.status != domain.TargetChannelStatus.ACTIVE: + channel.status = domain.TargetChannelStatus.ACTIVE await self.database.update_target_channel(channel) await self.telegram_bot.send_message( @@ -40,8 +40,8 @@ async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTarge log.info(f'Target channel {input.telegram_id} reactivated - all permissions granted') return - if channel.is_active: - channel.is_active = False + if channel.status == domain.TargetChannelStatus.ACTIVE: + channel.status = domain.TargetChannelStatus.INACTIVE await self.database.update_target_channel(channel) missed_permissions = '\n'.join(f'• {p}' for p in missing_permissions) diff --git a/tests/test_creatives.py b/tests/test_creatives.py index 06c7a8e..ef77b72 100644 --- a/tests/test_creatives.py +++ b/tests/test_creatives.py @@ -18,7 +18,7 @@ async def test_create_creative_success(usecases: usecase.Usecase, mock_database: title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -70,7 +70,7 @@ async def test_get_user_creatives(usecases: usecase.Usecase, mock_database: Mock title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -107,14 +107,14 @@ async def test_get_creatives_filtered_by_target_channel(usecases: usecase.Usecas title='Target 1', username='target1', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) target2 = domain.TargetChannel( telegram_id=-1001234567891, title='Target 2', username='target2', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target1) await mock_database.upsert_target_channel(target2) @@ -153,7 +153,7 @@ async def test_archive_creative(usecases: usecase.Usecase, mock_database: MockDa title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -192,7 +192,7 @@ async def test_update_creative(usecases: usecase.Usecase, mock_database: MockDat title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -223,7 +223,7 @@ async def test_delete_creative(usecases: usecase.Usecase, mock_database: MockDat title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) diff --git a/tests/test_external_channels.py b/tests/test_external_channels.py index c95b9dc..0d90c72 100644 --- a/tests/test_external_channels.py +++ b/tests/test_external_channels.py @@ -18,7 +18,7 @@ async def test_create_external_channel_with_targets(usecases: usecase.Usecase, m title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -72,7 +72,7 @@ async def test_get_external_channels_for_target(usecases: usecase.Usecase, mock_ title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -106,7 +106,7 @@ async def test_delete_external_channel(usecases: usecase.Usecase, mock_database: title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -139,14 +139,14 @@ async def test_update_external_channel_links(usecases: usecase.Usecase, mock_dat title='Target 1', username='target1', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) target2 = domain.TargetChannel( telegram_id=-1001234567891, title='Target 2', username='target2', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target1) await mock_database.upsert_target_channel(target2) diff --git a/tests/test_placements.py b/tests/test_placements.py index 9976d2c..bbaf87a 100644 --- a/tests/test_placements.py +++ b/tests/test_placements.py @@ -20,7 +20,7 @@ async def test_create_placement_with_public_invite_link(usecases: usecase.Usecas title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -86,7 +86,7 @@ async def test_create_placement_with_approval_invite_link( title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -160,7 +160,7 @@ async def test_get_user_placements(usecases: usecase.Usecase, mock_database: Moc title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -229,14 +229,14 @@ async def test_get_placements_filtered_by_target_channel( title='Target 1', username='target1', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) target2 = domain.TargetChannel( telegram_id=-1001234567891, title='Target 2', username='target2', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target1) await mock_database.upsert_target_channel(target2) @@ -308,7 +308,7 @@ async def test_get_placements_filtered_by_creative(usecases: usecase.Usecase, mo title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -380,7 +380,7 @@ async def test_archive_placement(usecases: usecase.Usecase, mock_database: MockD title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -441,7 +441,7 @@ async def test_update_placement(usecases: usecase.Usecase, mock_database: MockDa title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -505,7 +505,7 @@ async def test_delete_placement(usecases: usecase.Usecase, mock_database: MockDa title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -564,7 +564,7 @@ async def test_multiple_placements_same_creative(usecases: usecase.Usecase, mock title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) diff --git a/tests/test_subscriptions.py b/tests/test_subscriptions.py index d3ab6d2..3855140 100644 --- a/tests/test_subscriptions.py +++ b/tests/test_subscriptions.py @@ -19,7 +19,7 @@ async def test_handle_subscription_success(usecases: usecase.Usecase, mock_datab title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -93,7 +93,7 @@ async def test_handle_subscription_duplicate_ignored(usecases: usecase.Usecase, title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -193,7 +193,7 @@ async def test_multiple_users_subscription_same_placement( title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -267,7 +267,7 @@ async def test_subscription_tracking_across_different_placements( title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) diff --git a/tests/test_target_channels.py b/tests/test_target_channels.py index dc49536..ea5a752 100644 --- a/tests/test_target_channels.py +++ b/tests/test_target_channels.py @@ -29,7 +29,8 @@ async def test_connect_new_channel_success(usecases: usecase.Usecase, mock_datab assert result.telegram_id == -1001234567890 assert result.title == 'Test Channel' - assert result.is_active is True + assert result.status == domain.TargetChannelStatus.ACTIVE + assert result.is_active is True # проверка обратной совместимости # Проверяем, что канал сохранён channel = await mock_database.get_target_channel(user.id, telegram_id=-1001234567890) @@ -128,14 +129,14 @@ async def test_get_user_target_channels(usecases: usecase.Usecase, mock_database title='Channel 1', username='channel1', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) channel2 = domain.TargetChannel( telegram_id=-1001234567891, title='Channel 2', username='channel2', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(channel1) await mock_database.upsert_target_channel(channel2) @@ -156,7 +157,7 @@ async def test_disconnect_target_channel(usecases: usecase.Usecase, mock_databas title='Test Channel', username='testchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(channel) @@ -165,4 +166,4 @@ async def test_disconnect_target_channel(usecases: usecase.Usecase, mock_databas # Проверяем, что канал стал неактивным updated_channel = await mock_database.get_target_channel(user.id, channel_id=channel.id) assert updated_channel is not None - assert updated_channel.is_active is False + assert updated_channel.status == domain.TargetChannelStatus.INACTIVE diff --git a/tests/test_views.py b/tests/test_views.py index 8c31ede..40f7c96 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -19,7 +19,7 @@ async def test_fetch_views_no_ad_post_url(usecases: usecase.Usecase, mock_databa title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -78,7 +78,7 @@ async def test_fetch_views_api_unavailable(usecases: usecase.Usecase, mock_datab title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -144,7 +144,7 @@ async def test_update_views_manually(usecases: usecase.Usecase, mock_database: M title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -216,7 +216,7 @@ async def test_get_views_history_empty(usecases: usecase.Usecase, mock_database: title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -272,7 +272,7 @@ async def test_get_views_history_with_snapshots(usecases: usecase.Usecase, mock_ title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel) @@ -353,7 +353,7 @@ async def test_get_views_history_with_date_filter(usecases: usecase.Usecase, moc title='Target Channel', username='targetchannel', user_id=user.id, - is_active=True, + status=domain.TargetChannelStatus.ACTIVE, ) await mock_database.upsert_target_channel(target_channel)