подписки сразу отслеживаются
This commit is contained in:
149
migrations/models/11_20260128113726_update.py
Normal file
149
migrations/models/11_20260128113726_update.py
Normal file
@@ -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"
|
||||||
|
)
|
||||||
@@ -21,7 +21,7 @@ async def get_views_history(
|
|||||||
to_date: datetime.datetime | None = None,
|
to_date: datetime.datetime | None = None,
|
||||||
) -> Page[dto.PostViewsHistoryOutput]:
|
) -> Page[dto.PostViewsHistoryOutput]:
|
||||||
input_data = dto.GetViewsHistoryInput(
|
input_data = dto.GetViewsHistoryInput(
|
||||||
placement_post_id=placement_id,
|
placement_id=placement_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
from_date=from_date,
|
from_date=from_date,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from tortoise import fields
|
|||||||
from .base import TimestampedModel
|
from .base import TimestampedModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .placement_post import PlacementPost
|
from .placement import Placement
|
||||||
from .telegram_user import TelegramUser
|
from .telegram_user import TelegramUser
|
||||||
|
|
||||||
|
|
||||||
@@ -21,17 +21,17 @@ class Subscription(TimestampedModel):
|
|||||||
status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE)
|
status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE)
|
||||||
unsubscribed_at = fields.DatetimeField(null=True)
|
unsubscribed_at = fields.DatetimeField(null=True)
|
||||||
|
|
||||||
placement_post: fields.ForeignKeyRelation['PlacementPost'] = fields.ForeignKeyField(
|
placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
|
||||||
'models.PlacementPost', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
telegram_user: fields.ForeignKeyRelation['TelegramUser'] = fields.ForeignKeyField(
|
telegram_user: fields.ForeignKeyRelation['TelegramUser'] = fields.ForeignKeyField(
|
||||||
'models.TelegramUser', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
'models.TelegramUser', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
placement_post_id: UUID
|
placement_id: UUID
|
||||||
telegram_user_id: UUID
|
telegram_user_id: UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'subscription'
|
table = 'subscription'
|
||||||
unique_together = (('placement_post_id', 'telegram_user_id'),)
|
unique_together = (('placement_id', 'telegram_user_id'),)
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ class PostViewsHistoryOutput(pydantic.BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class GetViewsHistoryInput(pydantic.BaseModel):
|
class GetViewsHistoryInput(pydantic.BaseModel):
|
||||||
"""Получить историю просмотров для публикации."""
|
"""Получить историю просмотров для placement."""
|
||||||
|
|
||||||
placement_post_id: uuid.UUID
|
placement_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
from_date: datetime.datetime | None = None # Фильтр: с какой даты
|
from_date: datetime.datetime | None = None # Фильтр: с какой даты
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
|
|||||||
placement_id=placement.id,
|
placement_id=placement.id,
|
||||||
include_archived=True,
|
include_archived=True,
|
||||||
)
|
)
|
||||||
placement_post_ids = [post.id for post in placement_posts]
|
# Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок)
|
||||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids)
|
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]
|
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 {}
|
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]
|
views_count = views_map.get(placement_post.post.id, (None,))[0]
|
||||||
placement_post_output = _build_placement_post_output(
|
placement_post_output = _build_placement_post_output(
|
||||||
placement_post,
|
placement_post,
|
||||||
subscriptions_counts.get(placement_post.id, 0),
|
subscriptions_count,
|
||||||
views_count,
|
views_count,
|
||||||
)
|
)
|
||||||
if placement_post_output is not None:
|
if placement_post_output is not None:
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
|
|||||||
placement_ids,
|
placement_ids,
|
||||||
include_archived=True,
|
include_archived=True,
|
||||||
)
|
)
|
||||||
placement_post_ids = [post.id for post in placement_posts]
|
# Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок)
|
||||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids)
|
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]
|
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 {}
|
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]
|
views_count = views_map.get(placement_post.post.id, (None,))[0]
|
||||||
placement_post_output = _build_placement_post_output(
|
placement_post_output = _build_placement_post_output(
|
||||||
placement_post,
|
placement_post,
|
||||||
subscriptions_counts.get(placement_post.id, 0),
|
subscriptions_counts.get(placement_post.placement_id, 0),
|
||||||
views_count,
|
views_count,
|
||||||
)
|
)
|
||||||
if placement_post_output is None:
|
if placement_post_output is None:
|
||||||
|
|||||||
@@ -17,13 +17,9 @@ async def handle_subscription(
|
|||||||
first_name: str | None = None,
|
first_name: str | None = None,
|
||||||
last_name: str | None = None,
|
last_name: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
placement_post = await self.database.get_placement_post_by_invite_link(invite_link)
|
placement = await self.database.get_placement_by_invite_link(invite_link)
|
||||||
if not placement_post:
|
if not placement:
|
||||||
log.warning('PlacementPost not found for invite_link: %s', invite_link)
|
log.warning('Placement not found for invite_link: %s', invite_link)
|
||||||
return
|
|
||||||
|
|
||||||
if not placement_post.placement:
|
|
||||||
log.error('PlacementPost %s has no placement', placement_post.id)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id)
|
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)
|
await self.database.update_telegram_user(subscriber)
|
||||||
|
|
||||||
active_subscription = await self.database.get_active_subscription_by_subscriber_and_project(
|
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:
|
if active_subscription:
|
||||||
# Пользователь уже подписан на канал через другую публикацию
|
# Пользователь уже подписан на канал через другой placement
|
||||||
# Это не должно случиться (Telegram не даст подписаться дважды),
|
# Это не должно случиться (Telegram не даст подписаться дважды),
|
||||||
# но если случилось - логируем и игнорируем
|
# но если случилось - логируем и игнорируем
|
||||||
log.warning(
|
log.warning(
|
||||||
'User %s (telegram_id: %s) already has active subscription to channel %s 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_post %s',
|
'ignoring new subscription attempt via placement %s',
|
||||||
subscriber.id,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
placement_post.placement.project_id,
|
placement.project_id,
|
||||||
active_subscription.placement_post_id,
|
active_subscription.placement_id,
|
||||||
placement_post.id,
|
placement.id,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Проверяем, была ли раньше подписка через ЭТУ публикацию (для реактивации)
|
# Проверяем, была ли раньше подписка через ЭТОТ placement (для реактивации)
|
||||||
existing_sub = await self.database.get_subscription_by_subscriber_and_placement_post(
|
existing_sub = await self.database.get_subscription_by_subscriber_and_placement(
|
||||||
subscriber.id, placement_post.id
|
subscriber.id, placement.id
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_sub and existing_sub.status == domain.SubscriptionStatus.UNSUBSCRIBED:
|
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)
|
await self.database.update_subscription(existing_sub)
|
||||||
|
|
||||||
log.info(
|
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,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
placement_post.id,
|
placement.id,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
subscription = domain.Subscription(
|
subscription = domain.Subscription(
|
||||||
placement_post_id=placement_post.id,
|
placement_id=placement.id,
|
||||||
telegram_user_id=subscriber.id,
|
telegram_user_id=subscriber.id,
|
||||||
invite_link=invite_link,
|
invite_link=invite_link,
|
||||||
)
|
)
|
||||||
await self.database.create_subscription(subscription)
|
await self.database.create_subscription(subscription)
|
||||||
|
|
||||||
log.info(
|
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,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
placement_post.id,
|
placement.id,
|
||||||
invite_link,
|
invite_link,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_
|
|||||||
await self.database.update_subscription(subscription)
|
await self.database.update_subscription(subscription)
|
||||||
|
|
||||||
log.info(
|
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,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
subscription.placement_post_id,
|
subscription.placement_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,18 +14,31 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) ->
|
|||||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
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)
|
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
|
||||||
if not placement_post:
|
if not placement:
|
||||||
log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id)
|
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
||||||
raise domain.PlacementPostNotFound(input.placement_post_id)
|
raise domain.PlacementNotFound(input.placement_id)
|
||||||
|
|
||||||
if not placement_post.placement:
|
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
|
||||||
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_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 []
|
return []
|
||||||
|
|
||||||
histories = await self.database.get_views_history(
|
histories = await self.database.get_views_history(
|
||||||
|
|||||||
Reference in New Issue
Block a user