From 0b25566ffb60dcda98799efbb9425be68b3d2555 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Wed, 28 Jan 2026 11:42:09 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D1=80=D0=B0=D0=B7=D1=83=20=D0=BE=D1=82=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D1=8E=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/models/11_20260128113726_update.py | 149 ++++++++++++++++++ src/controller/http_v1/views.py | 2 +- src/domain/subscription.py | 10 +- src/dto/views.py | 4 +- src/usecase/purchase/get_placement.py | 6 +- src/usecase/purchase/get_placements.py | 6 +- .../subscription/handle_subscription.py | 40 +++-- .../subscription/handle_unsubscription.py | 4 +- src/usecase/views/get_views_history.py | 31 ++-- 9 files changed, 205 insertions(+), 47 deletions(-) create mode 100644 migrations/models/11_20260128113726_update.py diff --git a/migrations/models/11_20260128113726_update.py b/migrations/models/11_20260128113726_update.py new file mode 100644 index 0000000..0766b1a --- /dev/null +++ b/migrations/models/11_20260128113726_update.py @@ -0,0 +1,149 @@ +# ruff: noqa +# mypy: ignore-errors +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + -- 1. Drop old constraints + ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "uid_subscriptio_placeme_aba67c"; + ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_placemen_f35fb6bb"; + + -- 2. Add new column for placement_id + ALTER TABLE "subscription" ADD COLUMN "placement_id" UUID; + + -- 3. Populate placement_id from placement_post table + UPDATE "subscription" s + SET placement_id = pp.placement_id + FROM "placement_post" pp + WHERE s.placement_post_id = pp.id; + + -- 4. Make NOT NULL (all subscriptions should have a placement via placement_post) + ALTER TABLE "subscription" ALTER COLUMN "placement_id" SET NOT NULL; + + -- 5. Add FK constraint to placement table + ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_placemen_0b5d8f15" + FOREIGN KEY ("placement_id") REFERENCES "placement" ("id") ON DELETE CASCADE; + + -- 6. Create unique index + CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_75b40b" + ON "subscription" ("placement_id", "telegram_user_id"); + + -- 7. Drop old column + ALTER TABLE "subscription" DROP COLUMN "placement_post_id"; + """ + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + -- 1. Drop new constraints + DROP INDEX IF EXISTS "uid_subscriptio_placeme_75b40b"; + ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_placemen_0b5d8f15"; + + -- 2. Add back placement_post_id column + ALTER TABLE "subscription" ADD COLUMN "placement_post_id" UUID; + + -- 3. Populate from placement_post (best effort - takes first placement_post for the placement) + UPDATE "subscription" s + SET placement_post_id = pp.id + FROM "placement_post" pp + WHERE s.placement_id = pp.placement_id; + + -- 4. Make NOT NULL (may fail if no matching placement_post exists) + ALTER TABLE "subscription" ALTER COLUMN "placement_post_id" SET NOT NULL; + + -- 5. Add FK constraint back to placement_post + ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_placemen_f35fb6bb" + FOREIGN KEY ("placement_post_id") REFERENCES "placement_post" ("id") ON DELETE CASCADE; + + -- 6. Restore unique index + CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_aba67c" + ON "subscription" ("placement_post_id", "telegram_user_id"); + + -- 7. Drop new column + ALTER TABLE "subscription" DROP COLUMN "placement_id"; + """ + + +MODELS_STATE = ( + "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" + "al" +) diff --git a/src/controller/http_v1/views.py b/src/controller/http_v1/views.py index baeec9e..feaf478 100644 --- a/src/controller/http_v1/views.py +++ b/src/controller/http_v1/views.py @@ -21,7 +21,7 @@ async def get_views_history( to_date: datetime.datetime | None = None, ) -> Page[dto.PostViewsHistoryOutput]: input_data = dto.GetViewsHistoryInput( - placement_post_id=placement_id, + placement_id=placement_id, user_id=current_user.user_id, workspace_id=workspace_id, from_date=from_date, diff --git a/src/domain/subscription.py b/src/domain/subscription.py index e97a916..aeb367b 100644 --- a/src/domain/subscription.py +++ b/src/domain/subscription.py @@ -7,7 +7,7 @@ from tortoise import fields from .base import TimestampedModel if TYPE_CHECKING: - from .placement_post import PlacementPost + from .placement import Placement from .telegram_user import TelegramUser @@ -21,17 +21,17 @@ class Subscription(TimestampedModel): status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE) unsubscribed_at = fields.DatetimeField(null=True) - placement_post: fields.ForeignKeyRelation['PlacementPost'] = fields.ForeignKeyField( - 'models.PlacementPost', related_name='subscriptions', on_delete=fields.CASCADE, index=True + placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField( + 'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True ) telegram_user: fields.ForeignKeyRelation['TelegramUser'] = fields.ForeignKeyField( 'models.TelegramUser', related_name='subscriptions', on_delete=fields.CASCADE, index=True ) if TYPE_CHECKING: - placement_post_id: UUID + placement_id: UUID telegram_user_id: UUID class Meta: table = 'subscription' - unique_together = (('placement_post_id', 'telegram_user_id'),) + unique_together = (('placement_id', 'telegram_user_id'),) diff --git a/src/dto/views.py b/src/dto/views.py index c5dac33..9bc1602 100644 --- a/src/dto/views.py +++ b/src/dto/views.py @@ -15,9 +15,9 @@ class PostViewsHistoryOutput(pydantic.BaseModel): class GetViewsHistoryInput(pydantic.BaseModel): - """Получить историю просмотров для публикации.""" + """Получить историю просмотров для placement.""" - placement_post_id: uuid.UUID + placement_id: uuid.UUID user_id: uuid.UUID workspace_id: uuid.UUID from_date: datetime.datetime | None = None # Фильтр: с какой даты diff --git a/src/usecase/purchase/get_placement.py b/src/usecase/purchase/get_placement.py index a5a3a32..d413ce2 100644 --- a/src/usecase/purchase/get_placement.py +++ b/src/usecase/purchase/get_placement.py @@ -74,8 +74,8 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d placement_id=placement.id, include_archived=True, ) - placement_post_ids = [post.id for post in placement_posts] - subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) + # Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок) + subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id) post_ids = [placement_post.post.id for placement_post in placement_posts if placement_post.post] views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {} @@ -89,7 +89,7 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d views_count = views_map.get(placement_post.post.id, (None,))[0] placement_post_output = _build_placement_post_output( placement_post, - subscriptions_counts.get(placement_post.id, 0), + subscriptions_count, views_count, ) if placement_post_output is not None: diff --git a/src/usecase/purchase/get_placements.py b/src/usecase/purchase/get_placements.py index a8deabb..3f54568 100644 --- a/src/usecase/purchase/get_placements.py +++ b/src/usecase/purchase/get_placements.py @@ -34,8 +34,8 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto. placement_ids, include_archived=True, ) - placement_post_ids = [post.id for post in placement_posts] - subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) + # Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок) + subscriptions_counts = await self.database.count_subscriptions_by_placement_batch(placement_ids) post_ids = [placement_post.post.id for placement_post in placement_posts if placement_post.post] views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {} @@ -46,7 +46,7 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto. views_count = views_map.get(placement_post.post.id, (None,))[0] placement_post_output = _build_placement_post_output( placement_post, - subscriptions_counts.get(placement_post.id, 0), + subscriptions_counts.get(placement_post.placement_id, 0), views_count, ) if placement_post_output is None: diff --git a/src/usecase/subscription/handle_subscription.py b/src/usecase/subscription/handle_subscription.py index f8ccd54..8d6b1b2 100644 --- a/src/usecase/subscription/handle_subscription.py +++ b/src/usecase/subscription/handle_subscription.py @@ -17,13 +17,9 @@ async def handle_subscription( first_name: str | None = None, last_name: str | None = None, ) -> None: - placement_post = await self.database.get_placement_post_by_invite_link(invite_link) - if not placement_post: - log.warning('PlacementPost not found for invite_link: %s', invite_link) - return - - if not placement_post.placement: - log.error('PlacementPost %s has no placement', placement_post.id) + placement = await self.database.get_placement_by_invite_link(invite_link) + if not placement: + log.warning('Placement not found for invite_link: %s', invite_link) return subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id) @@ -42,27 +38,27 @@ async def handle_subscription( await self.database.update_telegram_user(subscriber) active_subscription = await self.database.get_active_subscription_by_subscriber_and_project( - subscriber.id, placement_post.placement.project_id + subscriber.id, placement.project_id ) if active_subscription: - # Пользователь уже подписан на канал через другую публикацию + # Пользователь уже подписан на канал через другой placement # Это не должно случиться (Telegram не даст подписаться дважды), # но если случилось - логируем и игнорируем log.warning( - 'User %s (telegram_id: %s) already has active subscription to channel %s via placement_post %s, ' - 'ignoring new subscription attempt via placement_post %s', + 'User %s (telegram_id: %s) already has active subscription to channel %s via placement %s, ' + 'ignoring new subscription attempt via placement %s', subscriber.id, user_telegram_id, - placement_post.placement.project_id, - active_subscription.placement_post_id, - placement_post.id, + placement.project_id, + active_subscription.placement_id, + placement.id, ) return - # Проверяем, была ли раньше подписка через ЭТУ публикацию (для реактивации) - existing_sub = await self.database.get_subscription_by_subscriber_and_placement_post( - subscriber.id, placement_post.id + # Проверяем, была ли раньше подписка через ЭТОТ placement (для реактивации) + existing_sub = await self.database.get_subscription_by_subscriber_and_placement( + subscriber.id, placement.id ) if existing_sub and existing_sub.status == domain.SubscriptionStatus.UNSUBSCRIBED: @@ -71,24 +67,24 @@ async def handle_subscription( await self.database.update_subscription(existing_sub) log.info( - 'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same placement_post %s', + 'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same placement %s', subscriber.id, user_telegram_id, - placement_post.id, + placement.id, ) return subscription = domain.Subscription( - placement_post_id=placement_post.id, + placement_id=placement.id, telegram_user_id=subscriber.id, invite_link=invite_link, ) await self.database.create_subscription(subscription) log.info( - 'Subscription created: subscriber %s (telegram_id: %s) subscribed via placement_post %s (invite_link: %s)', + 'Subscription created: subscriber %s (telegram_id: %s) subscribed via placement %s (invite_link: %s)', subscriber.id, user_telegram_id, - placement_post.id, + placement.id, invite_link, ) diff --git a/src/usecase/subscription/handle_unsubscription.py b/src/usecase/subscription/handle_unsubscription.py index 8f50ab6..0f25a46 100644 --- a/src/usecase/subscription/handle_unsubscription.py +++ b/src/usecase/subscription/handle_unsubscription.py @@ -39,8 +39,8 @@ async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_ await self.database.update_subscription(subscription) log.info( - 'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from placement_post %s', + 'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from placement %s', subscriber.id, user_telegram_id, - subscription.placement_post_id, + subscription.placement_id, ) diff --git a/src/usecase/views/get_views_history.py b/src/usecase/views/get_views_history.py index 164a8c9..c78b31c 100644 --- a/src/usecase/views/get_views_history.py +++ b/src/usecase/views/get_views_history.py @@ -14,18 +14,31 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ ) - placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id) - if not placement_post: - log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id) - raise domain.PlacementPostNotFound(input.placement_post_id) + placement = await self.database.get_placement(input.workspace_id, input.placement_id) + if not placement: + log.warning('Placement %s not found for user %s', input.placement_id, input.user_id) + raise domain.PlacementNotFound(input.placement_id) - if not placement_post.placement: - log.error('PlacementPost %s has no placement', placement_post.id) - raise domain.PlacementNotFound(placement_post.placement_id) + context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id) - context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement_post.placement.project_id) + # Получаем PlacementPost для этого Placement (для получения связанного Post) + placement_posts = await self.database.get_workspace_placement_posts( + input.workspace_id, + placement_id=placement.id, + include_archived=True, + ) - if not placement_post.post: + if not placement_posts: + return [] + + # Берём первый PlacementPost с постом + placement_post = None + for pp in placement_posts: + if pp.post: + placement_post = pp + break + + if not placement_post or not placement_post.post: return [] histories = await self.database.get_views_history(