инвайт линк и подсчет отписок
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# ruff: noqa
|
||||
# mypy: ignore-errors
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
RUN_IN_TRANSACTION = True
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "placement" ADD "invite_link_created_at" TIMESTAMPTZ;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "placement" DROP COLUMN "invite_link_created_at";"""
|
||||
|
||||
|
||||
MODELS_STATE = (
|
||||
"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"
|
||||
)
|
||||
@@ -733,6 +733,47 @@ class Postgres(DatabaseBase):
|
||||
counts = {row['placement_id']: row['count'] for row in results}
|
||||
return {pid: counts.get(pid, 0) for pid in placement_ids}
|
||||
|
||||
@staticmethod
|
||||
async def count_unsubscriptions_by_placement_post_batch(
|
||||
placement_post_ids: list[uuid.UUID],
|
||||
) -> dict[uuid.UUID, int]:
|
||||
"""Count unsubscriptions for placement_posts by finding their parent placements."""
|
||||
if not placement_post_ids:
|
||||
return {}
|
||||
|
||||
from tortoise.functions import Count
|
||||
|
||||
# Get placement_ids from placement_posts
|
||||
placement_posts = await domain.PlacementPost.filter(id__in=placement_post_ids).all()
|
||||
placement_id_to_post_ids: dict[uuid.UUID, list[uuid.UUID]] = {}
|
||||
for pp in placement_posts:
|
||||
placement_id_to_post_ids.setdefault(pp.placement_id, []).append(pp.id)
|
||||
|
||||
placement_ids = list(placement_id_to_post_ids.keys())
|
||||
if not placement_ids:
|
||||
return dict.fromkeys(placement_post_ids, 0)
|
||||
|
||||
# Count unsubscriptions by placement (filter by UNSUBSCRIBED status)
|
||||
results = (
|
||||
await domain.Subscription.filter(
|
||||
placement_id__in=placement_ids, status=domain.SubscriptionStatus.UNSUBSCRIBED
|
||||
)
|
||||
.group_by('placement_id')
|
||||
.annotate(count=Count('id'))
|
||||
.values('placement_id', 'count')
|
||||
)
|
||||
|
||||
placement_counts = {row['placement_id']: row['count'] for row in results}
|
||||
|
||||
# Map back to placement_post_ids
|
||||
post_counts: dict[uuid.UUID, int] = {}
|
||||
for placement_id, post_ids in placement_id_to_post_ids.items():
|
||||
count = placement_counts.get(placement_id, 0)
|
||||
for post_id in post_ids:
|
||||
post_counts[post_id] = count
|
||||
|
||||
return {pid: post_counts.get(pid, 0) for pid in placement_post_ids}
|
||||
|
||||
@staticmethod
|
||||
async def has_placement_posts_for_creative(creative_id: uuid.UUID) -> bool:
|
||||
return await domain.PlacementPost.filter(placement__creative_id=creative_id).exists()
|
||||
|
||||
@@ -54,6 +54,7 @@ class Placement(TimestampedModel):
|
||||
comment = fields.TextField(null=True)
|
||||
|
||||
invite_link = fields.CharField(max_length=512, null=True)
|
||||
invite_link_created_at = fields.DatetimeField(null=True)
|
||||
invite_link_type = fields.CharEnumField(InviteLinkType)
|
||||
|
||||
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||
|
||||
@@ -47,6 +47,7 @@ class PlacementAnalyticsOutput(pydantic.BaseModel):
|
||||
placement_date: datetime.datetime
|
||||
cost: float | None
|
||||
subscriptions_count: int
|
||||
unsubscriptions_count: int
|
||||
views_count: int | None
|
||||
cpf: float | None
|
||||
cpm: float | None
|
||||
|
||||
@@ -30,6 +30,7 @@ class PlacementOutput(pydantic.BaseModel):
|
||||
creative_id: uuid.UUID | None = None
|
||||
comment: str | None = None
|
||||
invite_link: str | None
|
||||
invite_link_created_at: datetime.datetime | None = None
|
||||
invite_link_type: InviteLinkType
|
||||
channel: ChannelOutput
|
||||
details: PlacementDetails | None = None
|
||||
|
||||
@@ -72,6 +72,7 @@ async def get_placements_analytics(
|
||||
# Batch fetch subscriptions counts
|
||||
placement_ids = [p.id for p in placements]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||
unsubscriptions_counts = await self.database.count_unsubscriptions_by_placement_post_batch(placement_ids)
|
||||
|
||||
results: list[dto.PlacementAnalyticsOutput] = []
|
||||
for placement_post in placements:
|
||||
@@ -92,6 +93,7 @@ async def get_placements_analytics(
|
||||
|
||||
cost = _get_cost(placement_post)
|
||||
subs_count = 0 if hide_subscriptions else subscriptions_counts.get(placement_post.id, 0)
|
||||
unsubs_count = 0 if hide_subscriptions else unsubscriptions_counts.get(placement_post.id, 0)
|
||||
if placement_post.post and placement_post.post.id in views_map:
|
||||
views_count = views_map[placement_post.post.id][0]
|
||||
else:
|
||||
@@ -109,6 +111,7 @@ async def get_placements_analytics(
|
||||
placement_date=_get_placement_date(placement_post),
|
||||
cost=cost,
|
||||
subscriptions_count=subs_count,
|
||||
unsubscriptions_count=unsubs_count,
|
||||
views_count=views_count,
|
||||
cpf=cpf,
|
||||
cpm=_calculate_cpm(cost, views_count),
|
||||
|
||||
@@ -4,6 +4,7 @@ import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
from tortoise import timezone
|
||||
|
||||
from src import domain, dto
|
||||
|
||||
@@ -87,6 +88,7 @@ async def build_placement_creative(
|
||||
requires_approval = placement.invite_link_type == domain.InviteLinkType.APPROVAL
|
||||
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
|
||||
placement.invite_link = invite_link
|
||||
placement.invite_link_created_at = timezone.now()
|
||||
await self.database.update_placement(placement)
|
||||
|
||||
invite_link = placement.invite_link
|
||||
|
||||
Reference in New Issue
Block a user